From 5a10133564e0a3df2b067053871e9b72cdde5632 Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Fri, 18 Oct 2024 18:47:12 +0530 Subject: [PATCH 01/41] update aea-config.yaml and service.yaml for babydegen --- .../valory/agents/optimus/aea-config.yaml | 254 ++++++----- packages/valory/services/optimus/service.yaml | 405 +++++++++++++++++- 2 files changed, 538 insertions(+), 121 deletions(-) diff --git a/packages/valory/agents/optimus/aea-config.yaml b/packages/valory/agents/optimus/aea-config.yaml index 01715ce..bdff5a3 100644 --- a/packages/valory/agents/optimus/aea-config.yaml +++ b/packages/valory/agents/optimus/aea-config.yaml @@ -1,13 +1,15 @@ -agent_name: optimus +agent_name: solana_trader author: valory version: 0.1.0 license: Apache-2.0 -description: An optimism liquidity trader agent. -aea_version: '>=1.19.0, <2.0.0' +description: Solana trader agent. +aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeigx5mdvnamsqfum5ut7htok2y5vsnu7lrvms5gfvqi7hmv7sfbo3a + README.md: bafybeibm2adzlongvgzyepiiymb3hxpsjb43qgr7j4uydebjzcpdrwm3om fingerprint_ignore_patterns: [] connections: +- eightballer/dcxt:0.1.0:bafybeihes2id6qmqqybvdqfpchu6h3i5mpnhq6lztfxp52twd22iwqxacq - valory/abci:0.1.0:bafybeiejymu4ul62zx6weoibnlsrfprfpjnplhjefz6sr6izgdr4sajlnu - valory/http_client:0.23.0:bafybeihi772xgzpqeipp3fhmvpct4y6e6tpjp4sogwqrnf3wqspgeilg4u - valory/http_server:0.22.0:bafybeihpgu56ovmq4npazdbh6y6ru5i7zuv6wvdglpxavsckyih56smu7m @@ -15,10 +17,11 @@ connections: - valory/ledger:0.19.0:bafybeigntoericenpzvwejqfuc3kqzo2pscs76qoygg5dbj6f4zxusru5e - valory/p2p_libp2p_client:0.1.0:bafybeid3xg5k2ol5adflqloy75ibgljmol6xsvzvezebsg7oudxeeolz7e contracts: -- valory/gnosis_safe:0.1.0:bafybeib375xmvcplw7ageic2np3hq4yqeijrvd5kl7rrdnyvswats6ngmm -- valory/gnosis_safe_proxy_factory:0.1.0:bafybeicpcpyurm7gxir2gnlsgzeirzomkhcbnzr5txk67zdf4mmg737rtu +- eightballer/erc_20:0.1.0:bafybeiezbnm3f5zhuj5bsc542isnlh2fki5q4nmm2vsajzps4uuoamofo4 - valory/multisend:0.1.0:bafybeig5byt5urg2d2bsecufxe5ql7f4mezg3mekfleeh32nmuusx66p4y - valory/service_registry:0.1.0:bafybeihafe524ilngwzavkhwz4er56p7nyar26lfm7lrksfiqvvzo3kdcq +- valory/gnosis_safe:0.1.0:bafybeiho6sbfts3zk3mftrngw37d5qnlvkqtnttt3fzexmcwkeevhu4wwi +- valory/gnosis_safe_proxy_factory:0.1.0:bafybeicpcpyurm7gxir2gnlsgzeirzomkhcbnzr5txk67zdf4mmg737rtu - valory/balancer_weighted_pool:0.1.0:bafybeidyjlrlq3jrbackewedwt5irokhjupxgpqfgur2ri426cap2oqt7a - valory/balancer_vault:0.1.0:bafybeie6twptrkqddget7pjijzob2c4jqmrrtpkwombneh35xx56djz4ru - valory/uniswap_v3_non_fungible_position_manager:0.1.0:bafybeigadr3nyx6tkrual7oqn2qiup35addfevromxjzzlvkiukpyhtz6y @@ -41,9 +44,16 @@ skills: - valory/reset_pause_abci:0.1.0:bafybeif4lgvbzsmzljesxbphycdv52ka7qnihyjrjpfaseclxadcmm6yiq - valory/termination_abci:0.1.0:bafybeiekkpo5qef5zaeagm3si6v45qxcojvtjqe4a5ceccvk4q7k3xi3bi - valory/transaction_settlement_abci:0.1.0:bafybeielv6eivt2z6nforq43xewl2vmpfwpdu2s2vfogobziljnwsclmlm +- valory/market_data_fetcher_abci:0.1.0:bafybeia3kld7ogbaolbxskys7r5ccolhm53fqi4tdkrwnvilfm7gn5ztcm +- valory/strategy_evaluator_abci:0.1.0:bafybeig2mx3abjgjhiizx2kez3462mcygjwlqj5d6jvnovcf7rwzaql43e +- valory/trader_abci:0.1.0:bafybeiccni66lhpc6nt4hirtakw3xzhera7kqkzmreuznrpuxpckuh455e +- valory/trader_decision_maker_abci:0.1.0:bafybeih7duxvbjevmeez4bvdpahgvefoik3hnpjq3ik5g4jaqbuw5rybtu +- valory/ipfs_package_downloader:0.1.0:bafybeid54srronvfqbvcdjgtuhmr4mbndjkpxtgzguykeg4p3wwj3zboyi +- valory/portfolio_tracker_abci:0.1.0:bafybeigzyhm3fzoxhggjdexryzqgskafoi6rec4ois34n3asodxn6j3txm default_ledger: ethereum required_ledgers: - ethereum +- solana default_routing: {} connection_private_key_paths: {} private_key_paths: {} @@ -69,16 +79,27 @@ logging_config: - logfile - console propagate: true -skill_exception_policy: stop_and_exit dependencies: + open-aea-ledger-cosmos: + version: ==1.55.0 + open-aea-ledger-solana: + version: ==1.55.0 open-aea-ledger-ethereum: + version: ==1.55.0 + open-aea-test-autonomy: + version: ==0.15.2 + pyalgotrade: + version: ==0.20 + open-aea-ledger-ethereum-tool: version: ==1.57.0 +skill_exception_policy: stop_and_exit +connection_exception_policy: just_log default_connection: null --- public_id: valory/abci:0.1.0 type: connection config: - target_skill_id: valory/optimus_abci:0.1.0 + target_skill_id: valory/trader_abci:0.1.0 host: ${str:localhost} port: ${int:26658} use_tendermint: ${bool:false} @@ -88,8 +109,8 @@ type: connection config: ledger_apis: ethereum: - address: ${str:https://virtual.mainnet.rpc.tenderly.co/85a9fd10-356e-4526-b1f6-7148366bf227} - chain_id: ${int:1} + address: ${str:https://base.blockpi.network/v1/rpc/public} + chain_id: ${int:8453} poa_chain: ${bool:false} default_gas_price_strategy: ${str:eip1559} base: @@ -98,7 +119,7 @@ config: poa_chain: ${bool:false} default_gas_price_strategy: ${str:eip1559} optimism: - address: ${str:https://virtual.optimism.rpc.tenderly.co/3baf4a62-2fa9-448a-91a6-5f6ab95c76be} + address: ${str:https://mainnet.optimism.io} chain_id: ${int:10} poa_chain: ${bool:false} default_gas_price_strategy: ${str:eip1559} @@ -113,133 +134,134 @@ cert_requests: - identifier: acn ledger_id: ethereum message_format: '{public_key}' - not_after: '2023-01-01' - not_before: '2022-01-01' + not_after: '2024-01-01' + not_before: '2023-01-01' public_key: ${str:02d3a830c9d6ea1ae91936951430dee11f4662f33118b02190693be835359a9d77} - save_path: .certs/acn_cosmos_11000.txt + save_path: .certs/acn_cosmos_9005.txt --- public_id: valory/http_server:0.22.0:bafybeicblltx7ha3ulthg7bzfccuqqyjmihhrvfeztlgrlcoxhr7kf6nbq type: connection config: host: 0.0.0.0 - target_skill_id: valory/optimus_abci:0.1.0 + target_skill_id: valory/trader_abci:0.1.0 --- -public_id: valory/optimus_abci:0.1.0 +public_id: valory/http_client:0.23.0 +type: connection +config: + host: ${str:127.0.0.1} + port: ${int:8000} + timeout: ${int:1200} +--- +public_id: eightballer/dcxt:0.1.0 +type: connection +config: + target_skill_id: eightballer/chained_dex_app:0.1.0 + exchanges: + - name: ${str:balancer} + key_path: ${str:ethereum_private_key.txt} + ledger_id: ${str:base} + rpc_url: ${str:https://base.blockpi.network/v1/rpc/public} + etherscan_api_key: ${str:YOUR_ETHERSCAN_API_KEY} +--- +public_id: valory/ipfs_package_downloader:0.1.0 +type: skill +models: + params: + args: + cleanup_freq: ${int:50} + timeout_limit: ${int:3} + file_hash_to_id: ${list:[["bafybeic2fpf5ozhkf5jgzmppmfsprqw5ayfx6spgl3owuws464n7mkhpqi",["sma_strategy"]]]} + component_yaml_filename: ${str:component.yaml} + entry_point_key: ${str:entry_point} + callable_keys: ${list:["run_callable","transform_callable","evaluate_callable"]} +--- +public_id: valory/trader_abci:0.1.0 type: skill models: benchmark_tool: args: - log_dir: ${str:/logs} + log_dir: ${str:/benchmarks} + get_balance: + args: + api_id: ${str:get_balance} + headers: + Content-Type: ${str:application/json} + method: ${str:POST} + parameters: ${dict:{}} + response_key: ${str:result:value} + response_type: ${str:int} + error_key: ${str:error:message} + error_type: ${str:str} + retries: ${int:5} + url: ${str:https://api.mainnet-beta.solana.com} + token_accounts: + args: + api_id: ${str:token_accounts} + headers: + Content-Type: ${str:application/json} + method: ${str:POST} + parameters: ${dict:{}} + response_key: ${str:result:value} + response_type: ${str:list} + error_key: ${str:error:message} + error_type: ${str:str} + retries: ${int:5} + url: ${str:https://api.mainnet-beta.solana.com} coingecko: args: token_price_endpoint: ${str:https://api.coingecko.com/api/v3/simple/token_price/{asset_platform_id}?contract_addresses={token_address}&vs_currencies=usd} - coin_price_endpoint: ${str:https://api.coingecko.com/api/v3/simple/price?ids={coin_id}&vs_currencies=usd} + coin_price_endpoint: ${str:https://api.coingecko.com/api/v3/coins/{token_id}/market_chart?vs_currency=usd&days=1} api_key: ${str:null} - requests_per_minute: ${int:30} + prices_field: ${str:prices} + requests_per_minute: ${int:5} credits: ${int:10000} rate_limited_code: ${int:429} - chain_to_platform_id_mapping: ${str:{"optimism":"optimistic-ethereum","base":"base","ethereum":"ethereum"}} + tx_settlement_proxy: + args: + api_id: ${str:tx_settlement_proxy} + headers: + Content-Type: ${str:application/json} + method: ${str:POST} + parameters: + amount: ${int:100000000} + slippageBps: ${int:5} + resendAmount: ${int:200} + timeoutInMs: ${int:120000} + priorityFee: ${int:5000000} + response_key: ${str:null} + response_type: ${str:dict} + retries: ${int:5} + url: ${str:http://localhost:3000/tx} params: args: - cleanup_history_depth: 1 - cleanup_history_depth_current: null - drand_public_key: 868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31 + setup: + all_participants: ${list:["0x0000000000000000000000000000000000000000"]} + consensus_threshold: ${int:null} + safe_contract_address: ${str:0x0000000000000000000000000000000000000000} + cleanup_history_depth: ${int:1} + cleanup_history_depth_current: ${int:null} + drand_public_key: ${str:868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31} genesis_config: - genesis_time: '2022-09-26T00:00:00.000000000Z' - chain_id: chain-c4daS1 + genesis_time: ${str:2022-09-26T00:00:00.000000000Z} + chain_id: ${str:chain-c4daS1} consensus_params: block: - max_bytes: '22020096' - max_gas: '-1' - time_iota_ms: '1000' + max_bytes: ${str:22020096} + max_gas: ${str:-1} + time_iota_ms: ${str:1000} evidence: - max_age_num_blocks: '100000' - max_age_duration: '172800000000000' - max_bytes: '1048576' + max_age_num_blocks: ${str:100000} + max_age_duration: ${str:172800000000000} + max_bytes: ${str:1048576} validator: - pub_key_types: - - ed25519 - version: {} - voting_power: '10' - keeper_timeout: 30.0 - max_attempts: 10 - max_healthcheck: 120 - multisend_address: ${str:0x0000000000000000000000000000000000000000} - termination_sleep: ${int:900} - init_fallback_gas: 0 - keeper_allowed_retries: 3 - reset_pause_duration: ${int:10} - on_chain_service_id: ${int:1} - reset_tendermint_after: ${int:10} - retry_attempts: 400 - retry_timeout: 3 - request_retry_delay: 1.0 - request_timeout: 10.0 - round_timeout_seconds: 30.0 - service_id: optimus - service_registry_address: ${str:null} - setup: - all_participants: ${list:["0x1aCD50F973177f4D320913a9Cc494A9c66922fdF"]} - consensus_threshold: ${int:null} - safe_contract_address: ${str:0x0000000000000000000000000000000000000000} - share_tm_config_on_startup: ${bool:false} - sleep_time: 1 - tendermint_check_sleep_delay: 3 - tendermint_com_url: ${str:http://localhost:8080} - tendermint_max_retries: 5 - tendermint_url: ${str:http://localhost:26657} - tendermint_p2p_url: ${str:localhost:26656} - use_termination: ${bool:false} - tx_timeout: 10.0 - validate_timeout: 1205 - finalize_timeout: 60.0 - history_check_timeout: 1205 - use_slashing: ${bool:false} - slash_cooldown_hours: ${int:3} - slash_threshold_amount: ${int:10000000000000000} - light_slash_unit_amount: ${int:5000000000000000} - serious_slash_unit_amount: ${int:8000000000000000} - multisend_batch_size: ${int:50} - ipfs_address: ${str:https://gateway.autonolas.tech/ipfs/} - default_chain_id: ${str:optimism} - termination_from_block: ${int:34088325} - allowed_dexs: ${list:["balancerPool", "UniswapV3"]} - initial_assets: ${str:{"ethereum":{"0x0000000000000000000000000000000000000000":"ETH","0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48":"USDC"}}} - safe_contract_addresses: ${str:{"ethereum":"0x0000000000000000000000000000000000000000","base":"0x07e27E181Df065141ee90a4DD43cE4113bc9853C","optimism":"0x07e27E181Df065141ee90a4DD43cE4113bc9853C"}} - merkl_fetch_campaigns_args: ${str:{"url":"https://api.merkl.xyz/v3/campaigns","creator":"","live":"true"}} - allowed_chains: ${list:["optimism","base"]} - gas_reserve: ${str:{"ethereum":1000,"optimism":1000,"base":1000}} - round_threshold: ${int:0} - apr_threshold: ${int:5} - min_balance_multiplier: ${int:5} - multisend_contract_addresses: ${str:{"ethereum":"0x998739BFdAAdde7C933B942a68053933098f9EDa","optimism":"0xbE5b0013D2712DC4faF07726041C27ecFdBC35AD","base":"0x998739BFdAAdde7C933B942a68053933098f9EDa"}} - lifi_advance_routes_url: ${str:https://li.quest/v1/advanced/routes} - lifi_fetch_step_transaction_url: ${str:https://li.quest/v1/advanced/stepTransaction} - lifi_check_status_url: ${str:https://li.quest/v1/status} - lifi_fetch_tools_url: ${str:https://li.quest/v1/tools} - slippage_for_swap: ${float:0.09} - balancer_vault_contract_addresses: ${str:{"optimism":"0xBA12222222228d8Ba445958a75a0704d566BF2C8","base":"0xBA12222222228d8Ba445958a75a0704d566BF2C8"}} - uniswap_position_manager_contract_addresses: ${str:{"optimism":"0xC36442b4a4522E871399CD717aBDD847Ab11FE88","base":"0x03a520b32C04BF3bEEf7BEb72E919cf822Ed34f1"}} - chain_to_chain_key_mapping: ${str:{"ethereum":"eth","optimism":"opt","base":"bas"}} - waiting_period_for_status_check: ${int:10} - max_num_of_retries: ${int:5} - reward_claiming_time_period: ${int:28800} - merkl_distributor_contract_addresses: ${str:{"optimism":"0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae","base":"0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae"}} - intermediate_tokens: ${str:{"ethereum":{"0x0000000000000000000000000000000000000000":{"symbol":"ETH","liquidity_provider":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"},"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2":{"symbol":"WETH","liquidity_provider":"0xF04a5cC80B1E94C69B48f5ee68a08CD2F09A7c3E"},"0xdAC17F958D2ee523a2206206994597C13D831ec7":{"symbol":"USDT","liquidity_provider":"0xcEe284F754E854890e311e3280b767F80797180d"},"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48":{"symbol":"USDC","liquidity_provider":"0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"},"0x6B175474E89094C44Da98b954EedeAC495271d0F":{"symbol":"DAI","liquidity_provider":"0x517F9dD285e75b599234F7221227339478d0FcC8"},"0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84":{"symbol":"stETH","liquidity_provider":"0x4028DAAC072e492d34a3Afdbef0ba7e35D8b55C4"},"0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0":{"symbol":"wstETH","liquidity_provider":"0x109830a1AAaD605BbF02a9dFA7B0B92EC2FB7dAa"},"0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599":{"symbol":"WBTC","liquidity_provider":"0xCBCdF9626bC03E24f779434178A73a0B4bad62eD"},"0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984":{"symbol":"UNI","liquidity_provider":"0x1d42064Fc4Beb5F8aAF85F4617AE8b3b5B8Bd801"}},"optimism":{"0x0000000000000000000000000000000000000000":{"symbol":"ETH","liquidity_provider":"0x4200000000000000000000000000000000000006"},"0x7F5c764cBc14f9669B88837ca1490cCa17c31607":{"symbol":"USDC.e","liquidity_provider":"0xD1F1baD4c9E6c44DeC1e9bF3B94902205c5Cd6C3"},"0x4200000000000000000000000000000000000006":{"symbol":"WETH","liquidity_provider":"0xBA12222222228d8Ba445958a75a0704d566BF2C8"},"0x94b008aA00579c1307B0EF2c499aD98a8ce58e58":{"symbol":"USDT","liquidity_provider":"0xA73C628eaf6e283E26A7b1f8001CF186aa4c0E8E"},"0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1":{"symbol":"DAI","liquidity_provider":"0x03aF20bDAaFfB4cC0A521796a223f7D85e2aAc31"},"0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb":{"symbol":"wstETH","liquidity_provider":"0x04F6C85A1B00F6D9B75f91FD23835974Cc07E65c"},"0x68f180fcCe6836688e9084f035309E29Bf0A2095":{"symbol":"WBTC","liquidity_provider":"0x078f358208685046a11C85e8ad32895DED33A249"},"0x76FB31fb4af56892A25e32cFC43De717950c9278":{"symbol":"AAVE","liquidity_provider":"0xf329e36C7bF6E5E86ce2150875a84Ce77f477375"},"0x4200000000000000000000000000000000000042":{"symbol":"OP","liquidity_provider":"0x2A82Ae142b2e62Cb7D10b55E323ACB1Cab663a26"}},"base":{"0x0000000000000000000000000000000000000000":{"symbol":"ETH","liquidity_provider":"0xd0b53D9277642d899DF5C87A3966A349A798F224"},"0x4200000000000000000000000000000000000006":{"symbol":"WETH","liquidity_provider":"0xBA12222222228d8Ba445958a75a0704d566BF2C8"},"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913":{"symbol":"USDC","liquidity_provider":"0x0B0A5886664376F59C351ba3f598C8A8B4D0A6f3"},"0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA":{"symbol":"USDbC","liquidity_provider":"0x0B25c51637c43decd6CC1C1e3da4518D54ddb528"},"0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb":{"symbol":"DAI","liquidity_provider":"0x927860797d07b1C46fbBe7f6f73D45C7E1BFBb27"},"0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452":{"symbol":"wstETH","liquidity_provider":"0x99CBC45ea5bb7eF3a5BC08FB1B7E56bB2442Ef0D"},"0xB6fe221Fe9EeF5aBa221c348bA20A1Bf5e73624c":{"symbol":"rETH","liquidity_provider":"0x95Fa1ddc9a78273f795e67AbE8f1Cd2Cd39831fF"},"0x532f27101965dd16442E59d40670FaF5eBB142E4":{"symbol":"BRETT","liquidity_provider":"0xBA3F945812a83471d709BCe9C3CA699A19FB46f7"}}}} - merkl_user_rewards_url: ${str:https://api.merkl.xyz/v3/userRewards} - tenderly_bundle_simulation_url: ${str:https://api.tenderly.co/api/v1/account/{tenderly_account_slug}/project/{tenderly_project_slug}/simulate-bundle} - tenderly_access_key: ${str:access_key} - tenderly_account_slug: ${str:account_slug} - tenderly_project_slug: ${str:project_slug} - chain_to_chain_id_mapping: ${str:{"optimism":10,"base":8453,"ethereum":1}} - staking_token_contract_address: ${str:0x88996bbdE7f982D93214881756840cE2c77C4992} - staking_activity_checker_contract_address: ${str:0x7Fd1F4b764fA41d19fe3f63C85d12bf64d2bbf68} - staking_threshold_period: ${int:5} - store_path: ${str:/data/} - assets_info_filename: ${str:assets.json} - pool_info_filename: ${str:current_pool.json} - gas_cost_info_filename: ${str:gas_costs.json} - min_swap_amount_threshold: ${int:10} - max_fee_percentage: ${float:0.02} - max_gas_percentage: ${float:0.25} - balancer_graphql_endpoints: ${str:{"optimism":"https://api.studio.thegraph.com/query/75376/balancer-optimism-v2/version/latest","base":"https://api.studio.thegraph.com/query/24660/balancer-base-v2/version/latest"}} + pub_key_types: ${list:["ed25519"]} + version: ${dict:{}} + voting_power: ${str:10} + init_fallback_gas: ${int:0} + keeper_allowed_retries: ${int:3} + keeper_timeout: ${float:30.0} + max_attempts: ${int:10} + reset_tendermint_after: ${int:2} + retry_attempts: ${int:400} + retry_timeout: ${int:3} + request \ No newline at end of file diff --git a/packages/valory/services/optimus/service.yaml b/packages/valory/services/optimus/service.yaml index 22638bf..355fc42 100644 --- a/packages/valory/services/optimus/service.yaml +++ b/packages/valory/services/optimus/service.yaml @@ -1,13 +1,14 @@ -name: optimus +name: solana_trader author: valory version: 0.1.0 -description: An optimism liquidity trader service. +description: A set of agents trading tokens on Solana. aea_version: '>=1.0.0, <2.0.0' license: Apache-2.0 -fingerprint: {} +fingerprint: + README.md: bafybeiasyvanypleay7yspri6igebqccpm3a7uvvrlna5yjkzdsnvcbu4q fingerprint_ignore_patterns: [] -agent: valory/optimus:0.1.0:bafybeida2scmw3qune3n6ru7tuzquuc3mxs2cfivzcncrtlj4ziadv4sqy -number_of_agents: 1 +agent: valory/solana_trader:0.1.0:bafybeifmeou6eckov6nu5ni64dzeogncav6yao5hftr3kaybijpk53tocq +number_of_agents: 4 deployment: {} --- public_id: valory/optimus_abci:0.1.0 @@ -127,3 +128,397 @@ cert_requests: not_before: '2022-01-01' public_key: ${ACN_NODE_PUBLIC_KEY:str:02d3a830c9d6ea1ae91936951430dee11f4662f33118b02190693be835359a9d77} save_path: .certs/acn_cosmos_11000.txt +--- +public_id: valory/ipfs_package_downloader:0.1.0 +type: skill +0: + models: + params: + args: + cleanup_freq: ${CLEANUP_FREQ:int:50} + timeout_limit: ${TIMEOUT_LIMIT:int:3} + file_hash_to_id: ${FILE_HASH_TO_ID:list:[["bafybeifpqcxwjjlenpa7n3nnx5ornrg7uz5d7h76ugzhiwk45bx3sx3cta",["sma_strategy"]]]} + component_yaml_filename: ${COMPONENT_YAML_FILENAME:str:component.yaml} + entry_point_key: ${ENTRY_POINT_KEY:str:entry_point} + callable_keys: ${CALLABLE_KEYS:list:["run_callable","transform_callable","evaluate_callable"]} +1: + models: + params: + args: + cleanup_freq: ${CLEANUP_FREQ:int:50} + timeout_limit: ${TIMEOUT_LIMIT:int:3} + file_hash_to_id: ${FILE_HASH_TO_ID:list:[["bafybeifpqcxwjjlenpa7n3nnx5ornrg7uz5d7h76ugzhiwk45bx3sx3cta",["sma_strategy"]]]} + component_yaml_filename: ${COMPONENT_YAML_FILENAME:str:component.yaml} + entry_point_key: ${ENTRY_POINT_KEY:str:entry_point} + callable_keys: ${CALLABLE_KEYS:list:["run_callable","transform_callable","evaluate_callable"]} +2: + models: + params: + args: + cleanup_freq: ${CLEANUP_FREQ:int:50} + timeout_limit: ${TIMEOUT_LIMIT:int:3} + file_hash_to_id: ${FILE_HASH_TO_ID:list:[["bafybeifpqcxwjjlenpa7n3nnx5ornrg7uz5d7h76ugzhiwk45bx3sx3cta",["sma_strategy"]]]} + component_yaml_filename: ${COMPONENT_YAML_FILENAME:str:component.yaml} + entry_point_key: ${ENTRY_POINT_KEY:str:entry_point} + callable_keys: ${CALLABLE_KEYS:list:["run_callable","transform_callable","evaluate_callable"]} +3: + models: + params: + args: + cleanup_freq: ${CLEANUP_FREQ:int:50} + timeout_limit: ${TIMEOUT_LIMIT:int:3} + file_hash_to_id: ${FILE_HASH_TO_ID:list:[["bafybeifpqcxwjjlenpa7n3nnx5ornrg7uz5d7h76ugzhiwk45bx3sx3cta",["sma_strategy"]]]} + component_yaml_filename: ${COMPONENT_YAML_FILENAME:str:component.yaml} + entry_point_key: ${ENTRY_POINT_KEY:str:entry_point} + callable_keys: ${CALLABLE_KEYS:list:["run_callable","transform_callable","evaluate_callable"]} +--- +public_id: valory/trader_abci:0.1.0 +type: skill +0: + models: + params: + args: + setup: &id002 + all_participants: ${ALL_PARTICIPANTS:list:[]} + safe_contract_address: ${SAFE_CONTRACT_ADDRESS:str:unknown111111111111111111111111111111111111} + consensus_threshold: ${CONSENSUS_THRESHOLD:int:null} + cleanup_history_depth: ${CLEANUP_HISTORY_DEPTH:int:1} + cleanup_history_depth_current: ${CLEANUP_HISTORY_DEPTH_CURRENT:int:null} + drand_public_key: ${DRAND_PUBLIC_KEY:str:868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31} + finalize_timeout: ${FINALIZE_TIMEOUT:float:60.0} + genesis_config: &id003 + genesis_time: ${GENESIS_TIME:str:'2023-07-12T00:00:00.000000000Z'} + chain_id: ${GENESIS_CHAIN_ID:str:chain-c4daS1} + consensus_params: + block: + max_bytes: ${BLOCK_MAX_BYTES:str:'22020096'} + max_gas: ${MAX_GAS:str:'-1'} + time_iota_ms: ${TIME_IOTA_MS:str:'1000'} + evidence: + max_age_num_blocks: ${MAX_AGE_NUM_BLOCKS:str:'100000'} + max_age_duration: ${MAX_AGE_DURATION:str:'172800000000000'} + max_bytes: ${EVIDENCE_MAX_BYTES:str:'1048576'} + validator: + pub_key_types: ${PUB_KEY_TYPES:list:["ed25519"]} + version: ${VERSION:dict:{}} + voting_power: ${VOTING_POWER:str:'10'} + init_fallback_gas: ${INIT_FALLBACK_GAS:int:0} + keeper_allowed_retries: ${KEEPER_ALLOWED_RETRIES:int:3} + keeper_timeout: ${KEEPER_TIMEOUT:float:30.0} + max_attempts: ${MAX_ATTEMPTS:int:10} + max_healthcheck: ${MAX_HEALTHCHECK:int:120} + multisend_address: ${MULTISEND_ADDRESS:str:unknown111111111111111111111111111111111111} + on_chain_service_id: ${ON_CHAIN_SERVICE_ID:int:null} + reset_tendermint_after: ${RESET_TM_AFTER:int:200} + retry_attempts: ${RETRY_ATTEMPTS:int:400} + retry_timeout: ${RETRY_TIMEOUT:int:3} + reset_pause_duration: ${RESET_PAUSE_DURATION:int:60} + request_retry_delay: ${REQUEST_RETRY_DELAY:float:1.0} + request_timeout: ${REQUEST_TIMEOUT:float:10.0} + round_timeout_seconds: ${ROUND_TIMEOUT:float:350.0} + proxy_round_timeout_seconds: ${PROXY_ROUND_TIMEOUT:float:1200.0} + service_id: ${SERVICE_ID:str:solana_trader} + service_registry_address: ${SERVICE_REGISTRY_ADDRESS:str:unknown111111111111111111111111111111111111} + agent_registry_address: ${AGENT_REGISTRY_ADDRESS:str:unknown111111111111111111111111111111111111} + share_tm_config_on_startup: ${USE_ACN:bool:false} + sleep_time: ${SLEEP_TIME:int:1} + tendermint_check_sleep_delay: ${TM_CHECK_SLEEP_DELAY:int:3} + tendermint_com_url: ${TENDERMINT_COM_URL:str:http://localhost:8080} + tendermint_max_retries: ${TM_MAX_RETRIES:int:5} + tendermint_url: ${TENDERMINT_URL:str:http://localhost:26657} + tendermint_p2p_url: ${TM_P2P_ENDPOINT_NODE_0:str:node0:26656} + termination_sleep: ${TERMINATION_SLEEP:int:900} + tx_timeout: ${TX_TIMEOUT:float:10.0} + use_termination: ${USE_TERMINATION:bool:false} + validate_timeout: ${VALIDATE_TIMEOUT:int:1205} + history_check_timeout: ${HISTORY_CHECK_TIMEOUT:int:1205} + token_symbol_whitelist: ${TOKEN_SYMBOL_WHITELIST:list:["coingecko_id=solana&address=So11111111111111111111111111111111111111112"]} + strategies_kwargs: ${STRATEGIES_KWARGS:list:[["ma_period",20],["rsi_period",14],["rsi_overbought_threshold",70],["rsi_oversold_threshold",30]]} + use_proxy_server: ${USE_PROXY_SERVER:bool:false} + expected_swap_tx_cost: ${EXPECTED_SWAP_TX_COST:int:20000000} + ipfs_fetch_retries: ${IPFS_FETCH_RETRIES:int:5} + squad_vault: ${SQUAD_VAULT:str:39Zh4C687EXLY7CT8gjCxe2hUc3krESjUsqs7A1CKD5E} + agent_balance_threshold: ${AGENT_BALANCE_THRESHOLD:int:50000000} + multisig_balance_threshold: ${MULTISIG_BALANCE_THRESHOLD:int:1000000000} + tracked_tokens: ${TRACKED_TOKENS:list:[]} + refill_action_timeout: ${REFILL_ACTION_TIMEOUT:int:10} + rpc_polling_interval: ${RPC_POLLING_INTERVAL:int:5} + epsilon: ${EPSILON:float:0.1} + sharpe_threshold: ${SHARPE_THRESHOLD:float:1.0} + ledger_ids: ${LEDGER_IDS:list:["ethereum"]} + trade_size_in_base_token: ${TRADE_SIZE_IN_BASE_TOKEN:float:0.0001} + benchmark_tool: &id004 + args: + log_dir: ${LOG_DIR:str:/benchmarks} + get_balance: &id001 + args: + url: ${SOLANA_RPC:str:replace_with_a_solana_rpc} + token_accounts: *id001 + coingecko: &id005 + args: + endpoint: ${COINGECKO_ENDPOINT:str:https://api.coingecko.com/api/v3/coins/{token_id}/market_chart?vs_currency=usd&days=1} + api_key: ${COINGECKO_API_KEY:str:null} + prices_field: ${COINGECKO_PRICES_FIELD:str:prices} + requests_per_minute: ${COINGECKO_REQUESTS_PER_MINUTE:int:5} + credits: ${COINGECKO_CREDITS:int:10000} + rate_limited_code: ${COINGECKO_RATE_LIMITED_CODE:int:429} + tx_settlement_proxy: &id006 + args: + parameters: + amount: ${TX_PROXY_SWAP_AMOUNT:int:100000000} + slippageBps: ${TX_PROXY_SLIPPAGE_BPS:int:5} + resendAmount: ${TX_PROXY_SPAM_AMOUNT:int:200} + timeoutInMs: ${TX_PROXY_VERIFICATION_TIMEOUT_MS:int:120000} + priorityFee: ${TX_PROXY_PRIORITY_FEE:int:5000000} + response_key: ${TX_PROXY_RESPONSE_KEY:str:null} + response_type: ${TX_PROXY_RESPONSE_TYPE:str:dict} + retries: ${TX_PROXY_RETRIES:int:5} + url: ${TX_PROXY_URL:str:http://localhost:3000/tx} +1: + models: + params: + args: + setup: *id002 + cleanup_history_depth: ${CLEANUP_HISTORY_DEPTH:int:1} + cleanup_history_depth_current: ${CLEANUP_HISTORY_DEPTH_CURRENT:int:null} + drand_public_key: ${DRAND_PUBLIC_KEY:str:868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31} + finalize_timeout: ${FINALIZE_TIMEOUT:float:60.0} + genesis_config: *id003 + init_fallback_gas: ${INIT_FALLBACK_GAS:int:0} + keeper_allowed_retries: ${KEEPER_ALLOWED_RETRIES:int:3} + keeper_timeout: ${KEEPER_TIMEOUT:float:30.0} + max_attempts: ${MAX_ATTEMPTS:int:10} + max_healthcheck: ${MAX_HEALTHCHECK:int:120} + multisend_address: ${MULTISEND_ADDRESS:str:unknown111111111111111111111111111111111111} + on_chain_service_id: ${ON_CHAIN_SERVICE_ID:int:null} + reset_tendermint_after: ${RESET_TM_AFTER:int:200} + retry_attempts: ${RETRY_ATTEMPTS:int:400} + retry_timeout: ${RETRY_TIMEOUT:int:3} + reset_pause_duration: ${RESET_PAUSE_DURATION:int:60} + request_retry_delay: ${REQUEST_RETRY_DELAY:float:1.0} + request_timeout: ${REQUEST_TIMEOUT:float:10.0} + round_timeout_seconds: ${ROUND_TIMEOUT:float:350.0} + proxy_round_timeout_seconds: ${PROXY_ROUND_TIMEOUT:float:1200.0} + service_id: ${SERVICE_ID:str:solana_trader} + service_registry_address: ${SERVICE_REGISTRY_ADDRESS:str:unknown111111111111111111111111111111111111} + agent_registry_address: ${AGENT_REGISTRY_ADDRESS:str:unknown111111111111111111111111111111111111} + share_tm_config_on_startup: ${USE_ACN:bool:false} + sleep_time: ${SLEEP_TIME:int:1} + tendermint_check_sleep_delay: ${TM_CHECK_SLEEP_DELAY:int:3} + tendermint_com_url: ${TENDERMINT_COM_URL:str:http://localhost:8080} + tendermint_max_retries: ${TM_MAX_RETRIES:int:5} + tendermint_url: ${TENDERMINT_URL:str:http://localhost:26657} + tendermint_p2p_url: ${TM_P2P_ENDPOINT_NODE_0:str:node0:26656} + termination_sleep: ${TERMINATION_SLEEP:int:900} + tx_timeout: ${TX_TIMEOUT:float:10.0} + use_termination: ${USE_TERMINATION:bool:false} + validate_timeout: ${VALIDATE_TIMEOUT:int:1205} + history_check_timeout: ${HISTORY_CHECK_TIMEOUT:int:1205} + token_symbol_whitelist: ${TOKEN_SYMBOL_WHITELIST:list:["coingecko_id=solana&address=So11111111111111111111111111111111111111112"]} + strategies_kwargs: ${STRATEGIES_KWARGS:list:[["ma_period",20],["rsi_period",14],["rsi_overbought_threshold",70],["rsi_oversold_threshold",30]]} + use_proxy_server: ${USE_PROXY_SERVER:bool:false} + expected_swap_tx_cost: ${EXPECTED_SWAP_TX_COST:int:20000000} + ipfs_fetch_retries: ${IPFS_FETCH_RETRIES:int:5} + squad_vault: ${SQUAD_VAULT:str:39Zh4C687EXLY7CT8gjCxe2hUc3krESjUsqs7A1CKD5E} + agent_balance_threshold: ${AGENT_BALANCE_THRESHOLD:int:50000000} + multisig_balance_threshold: ${MULTISIG_BALANCE_THRESHOLD:int:1000000000} + tracked_tokens: ${TRACKED_TOKENS:list:[]} + refill_action_timeout: ${REFILL_ACTION_TIMEOUT:int:10} + rpc_polling_interval: ${RPC_POLLING_INTERVAL:int:5} + epsilon: ${EPSILON:float:0.1} + sharpe_threshold: ${SHARPE_THRESHOLD:float:1.0} + ledger_ids: ${LEDGER_IDS:list:["ethereum"]} + trade_size_in_base_token: ${TRADE_SIZE_IN_BASE_TOKEN:float:0.0001} + benchmark_tool: *id004 + get_balance: *id001 + token_accounts: *id001 + coingecko: *id005 + tx_settlement_proxy: *id006 +2: + models: + params: + args: + setup: *id002 + cleanup_history_depth: ${CLEANUP_HISTORY_DEPTH:int:1} + cleanup_history_depth_current: ${CLEANUP_HISTORY_DEPTH_CURRENT:int:null} + drand_public_key: ${DRAND_PUBLIC_KEY:str:868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31} + finalize_timeout: ${FINALIZE_TIMEOUT:float:60.0} + genesis_config: *id003 + init_fallback_gas: ${INIT_FALLBACK_GAS:int:0} + keeper_allowed_retries: ${KEEPER_ALLOWED_RETRIES:int:3} + keeper_timeout: ${KEEPER_TIMEOUT:float:30.0} + max_attempts: ${MAX_ATTEMPTS:int:10} + max_healthcheck: ${MAX_HEALTHCHECK:int:120} + multisend_address: ${MULTISEND_ADDRESS:str:unknown111111111111111111111111111111111111} + on_chain_service_id: ${ON_CHAIN_SERVICE_ID:int:null} + reset_tendermint_after: ${RESET_TM_AFTER:int:200} + retry_attempts: ${RETRY_ATTEMPTS:int:400} + retry_timeout: ${RETRY_TIMEOUT:int:3} + reset_pause_duration: ${RESET_PAUSE_DURATION:int:60} + request_retry_delay: ${REQUEST_RETRY_DELAY:float:1.0} + request_timeout: ${REQUEST_TIMEOUT:float:10.0} + round_timeout_seconds: ${ROUND_TIMEOUT:float:350.0} + proxy_round_timeout_seconds: ${PROXY_ROUND_TIMEOUT:float:1200.0} + service_id: ${SERVICE_ID:str:solana_trader} + service_registry_address: ${SERVICE_REGISTRY_ADDRESS:str:unknown111111111111111111111111111111111111} + agent_registry_address: ${AGENT_REGISTRY_ADDRESS:str:unknown111111111111111111111111111111111111} + share_tm_config_on_startup: ${USE_ACN:bool:false} + sleep_time: ${SLEEP_TIME:int:1} + tendermint_check_sleep_delay: ${TM_CHECK_SLEEP_DELAY:int:3} + tendermint_com_url: ${TENDERMINT_COM_URL:str:http://localhost:8080} + tendermint_max_retries: ${TM_MAX_RETRIES:int:5} + tendermint_url: ${TENDERMINT_URL:str:http://localhost:26657} + tendermint_p2p_url: ${TM_P2P_ENDPOINT_NODE_0:str:node0:26656} + termination_sleep: ${TERMINATION_SLEEP:int:900} + tx_timeout: ${TX_TIMEOUT:float:10.0} + use_termination: ${USE_TERMINATION:bool:false} + validate_timeout: ${VALIDATE_TIMEOUT:int:1205} + history_check_timeout: ${HISTORY_CHECK_TIMEOUT:int:1205} + token_symbol_whitelist: ${TOKEN_SYMBOL_WHITELIST:list:["coingecko_id=solana&address=So11111111111111111111111111111111111111112"]} + strategies_kwargs: ${STRATEGIES_KWARGS:list:[["ma_period",20],["rsi_period",14],["rsi_overbought_threshold",70],["rsi_oversold_threshold",30]]} + use_proxy_server: ${USE_PROXY_SERVER:bool:false} + expected_swap_tx_cost: ${EXPECTED_SWAP_TX_COST:int:20000000} + ipfs_fetch_retries: ${IPFS_FETCH_RETRIES:int:5} + squad_vault: ${SQUAD_VAULT:str:39Zh4C687EXLY7CT8gjCxe2hUc3krESjUsqs7A1CKD5E} + agent_balance_threshold: ${AGENT_BALANCE_THRESHOLD:int:50000000} + multisig_balance_threshold: ${MULTISIG_BALANCE_THRESHOLD:int:1000000000} + tracked_tokens: ${TRACKED_TOKENS:list:[]} + refill_action_timeout: ${REFILL_ACTION_TIMEOUT:int:10} + rpc_polling_interval: ${RPC_POLLING_INTERVAL:int:5} + epsilon: ${EPSILON:float:0.1} + sharpe_threshold: ${SHARPE_THRESHOLD:float:1.0} + ledger_ids: ${LEDGER_IDS:list:["ethereum"]} + trade_size_in_base_token: ${TRADE_SIZE_IN_BASE_TOKEN:float:0.0001} + benchmark_tool: *id004 + get_balance: *id001 + token_accounts: *id001 + coingecko: *id005 + tx_settlement_proxy: *id006 +3: + models: + params: + args: + setup: *id002 + cleanup_history_depth: ${CLEANUP_HISTORY_DEPTH:int:1} + cleanup_history_depth_current: ${CLEANUP_HISTORY_DEPTH_CURRENT:int:null} + drand_public_key: ${DRAND_PUBLIC_KEY:str:868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31} + finalize_timeout: ${FINALIZE_TIMEOUT:float:60.0} + genesis_config: *id003 + init_fallback_gas: ${INIT_FALLBACK_GAS:int:0} + keeper_allowed_retries: ${KEEPER_ALLOWED_RETRIES:int:3} + keeper_timeout: ${KEEPER_TIMEOUT:float:30.0} + max_attempts: ${MAX_ATTEMPTS:int:10} + max_healthcheck: ${MAX_HEALTHCHECK:int:120} + multisend_address: ${MULTISEND_ADDRESS:str:unknown111111111111111111111111111111111111} + on_chain_service_id: ${ON_CHAIN_SERVICE_ID:int:null} + reset_tendermint_after: ${RESET_TM_AFTER:int:200} + retry_attempts: ${RETRY_ATTEMPTS:int:400} + retry_timeout: ${RETRY_TIMEOUT:int:3} + reset_pause_duration: ${RESET_PAUSE_DURATION:int:60} + request_retry_delay: ${REQUEST_RETRY_DELAY:float:1.0} + request_timeout: ${REQUEST_TIMEOUT:float:10.0} + round_timeout_seconds: ${ROUND_TIMEOUT:float:350.0} + proxy_round_timeout_seconds: ${PROXY_ROUND_TIMEOUT:float:1200.0} + service_id: ${SERVICE_ID:str:solana_trader} + service_registry_address: ${SERVICE_REGISTRY_ADDRESS:str:unknown111111111111111111111111111111111111} + agent_registry_address: ${AGENT_REGISTRY_ADDRESS:str:unknown111111111111111111111111111111111111} + share_tm_config_on_startup: ${USE_ACN:bool:false} + sleep_time: ${SLEEP_TIME:int:1} + tendermint_check_sleep_delay: ${TM_CHECK_SLEEP_DELAY:int:3} + tendermint_com_url: ${TENDERMINT_COM_URL:str:http://localhost:8080} + tendermint_max_retries: ${TM_MAX_RETRIES:int:5} + tendermint_url: ${TENDERMINT_URL:str:http://localhost:26657} + tendermint_p2p_url: ${TM_P2P_ENDPOINT_NODE_0:str:node0:26656} + termination_sleep: ${TERMINATION_SLEEP:int:900} + tx_timeout: ${TX_TIMEOUT:float:10.0} + use_termination: ${USE_TERMINATION:bool:false} + validate_timeout: ${VALIDATE_TIMEOUT:int:1205} + history_check_timeout: ${HISTORY_CHECK_TIMEOUT:int:1205} + token_symbol_whitelist: ${TOKEN_SYMBOL_WHITELIST:list:["coingecko_id=solana&address=So11111111111111111111111111111111111111112"]} + strategies_kwargs: ${STRATEGIES_KWARGS:list:[["ma_period",20],["rsi_period",14],["rsi_overbought_threshold",70],["rsi_oversold_threshold",30]]} + use_proxy_server: ${USE_PROXY_SERVER:bool:false} + expected_swap_tx_cost: ${EXPECTED_SWAP_TX_COST:int:20000000} + ipfs_fetch_retries: ${IPFS_FETCH_RETRIES:int:5} + squad_vault: ${SQUAD_VAULT:str:39Zh4C687EXLY7CT8gjCxe2hUc3krESjUsqs7A1CKD5E} + agent_balance_threshold: ${AGENT_BALANCE_THRESHOLD:int:50000000} + multisig_balance_threshold: ${MULTISIG_BALANCE_THRESHOLD:int:1000000000} + tracked_tokens: ${TRACKED_TOKENS:list:[]} + refill_action_timeout: ${REFILL_ACTION_TIMEOUT:int:10} + rpc_polling_interval: ${RPC_POLLING_INTERVAL:int:5} + epsilon: ${EPSILON:float:0.1} + sharpe_threshold: ${SHARPE_THRESHOLD:float:1.0} + ledger_ids: ${LEDGER_IDS:list:["ethereum"]} + trade_size_in_base_token: ${TRADE_SIZE_IN_BASE_TOKEN:float:0.0001} + benchmark_tool: *id004 + get_balance: *id001 + token_accounts: *id001 + coingecko: *id005 + tx_settlement_proxy: *id006 +--- +public_id: valory/ledger:0.19.0 +type: connection +0: + config: + ledger_apis: + ethereum: + address: ${RPC_0:str:http://host.docker.internal:8545} + chain_id: ${CHAIN_ID:int:1399811149} + default_gas_price_strategy: ${DEFAULT_GAS_PRICE_STRATEGY:str:eip1559} + poa_chain: ${POA_CHAIN:bool:false} +1: + config: + ledger_apis: + ethereum: + address: ${RPC_1:str:http://host.docker.internal:8545} + chain_id: ${CHAIN_ID:int:1399811149} + default_gas_price_strategy: ${DEFAULT_GAS_PRICE_STRATEGY:str:eip1559} + poa_chain: ${POA_CHAIN:bool:false} +2: + config: + ledger_apis: + ethereum: + address: ${RPC_2:str:http://host.docker.internal:8545} + chain_id: ${CHAIN_ID:int:1399811149} + default_gas_price_strategy: ${DEFAULT_GAS_PRICE_STRATEGY:str:eip1559} + poa_chain: ${POA_CHAIN:bool:false} +3: + config: + ledger_apis: + ethereum: + address: ${RPC_3:str:http://host.docker.internal:8545} + chain_id: ${CHAIN_ID:int:1399811149} + default_gas_price_strategy: ${DEFAULT_GAS_PRICE_STRATEGY:str:eip1559} + poa_chain: ${POA_CHAIN:bool:false} +--- +public_id: valory/p2p_libp2p_client:0.1.0 +type: connection +config: + nodes: + - uri: ${ACN_URI:str:acn.staging.autonolas.tech:9005} + public_key: ${ACN_NODE_PUBLIC_KEY:str:02d3a830c9d6ea1ae91936951430dee11f4662f33118b02190693be835359a9d77} +cert_requests: +- identifier: acn + ledger_id: ethereum + message_format: '{public_key}' + not_after: '2023-01-01' + not_before: '2022-01-01' + public_key: ${ACN_NODE_PUBLIC_KEY:str:02d3a830c9d6ea1ae91936951430dee11f4662f33118b02190693be835359a9d77} + save_path: .certs/acn_cosmos_11000.txt +--- +public_id: valory/http_client:0.23.0 +type: connection +config: + host: ${HTTP_CLIENT_HOST:str:127.0.0.1} + port: ${HTTP_CLIENT_PORT:int:8000} + timeout: ${HTTP_CLIENT_TIMEOUT:int:1200} +--- +public_id: eightballer/dcxt:0.1.0 +type: connection +config: + target_skill_id: valory/trader_abci:0.1.0 + exchanges: + - name: ${DCXT_EXCHANGE_NAME:str:balancer} + key_path: ${DCXT_KEY_PATH:str:ethereum_private_key.txt} + ledger_id: ${DCXT_LEDGER_ID:str:ethereum} + rpc_url: ${DCXT_RPC_URL:str:http://host.docker.internal:8545} + etherscan_api_key: ${DCXT_ETHERSCAN_API_KEY:str:null} \ No newline at end of file From c022bdff65530d1dc6a037b3d173e3989a51daec Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Fri, 18 Oct 2024 19:55:59 +0530 Subject: [PATCH 02/41] feat: Add ABCI skills from babydegen to optimus --- .gitignore | 2 +- .../agents/optimus/aea-config-optimus.yaml | 267 ++ packages/valory/skills/__init__.py | 20 + .../valory/skills/abstract_abci/README.md | 20 + .../valory/skills/abstract_abci/__init__.py | 25 + .../valory/skills/abstract_abci/dialogues.py | 61 + .../valory/skills/abstract_abci/handlers.py | 408 ++ .../valory/skills/abstract_abci/skill.yaml | 34 + .../skills/abstract_abci/tests/__init__.py | 20 + .../abstract_abci/tests/test_dialogues.py | 47 + .../abstract_abci/tests/test_handlers.py | 398 ++ .../skills/abstract_round_abci/README.md | 46 + .../skills/abstract_round_abci/__init__.py | 25 + .../abstract_round_abci/abci_app_chain.py | 291 ++ .../valory/skills/abstract_round_abci/base.py | 3850 +++++++++++++++++ .../abstract_round_abci/behaviour_utils.py | 2356 ++++++++++ .../skills/abstract_round_abci/behaviours.py | 409 ++ .../skills/abstract_round_abci/common.py | 231 + .../skills/abstract_round_abci/dialogues.py | 368 ++ .../skills/abstract_round_abci/handlers.py | 790 ++++ .../abstract_round_abci/io_/__init__.py | 20 + .../skills/abstract_round_abci/io_/ipfs.py | 85 + .../skills/abstract_round_abci/io_/load.py | 124 + .../skills/abstract_round_abci/io_/paths.py | 34 + .../skills/abstract_round_abci/io_/store.py | 153 + .../skills/abstract_round_abci/models.py | 893 ++++ .../skills/abstract_round_abci/skill.yaml | 164 + .../test_tools/__init__.py | 20 + .../test_tools/abci_app.py | 203 + .../abstract_round_abci/test_tools/base.py | 444 ++ .../abstract_round_abci/test_tools/common.py | 432 ++ .../test_tools/integration.py | 301 ++ .../abstract_round_abci/test_tools/rounds.py | 599 +++ .../abstract_round_abci/tests/__init__.py | 27 + .../abstract_round_abci/tests/conftest.py | 116 + .../tests/data/__init__.py | 20 + .../tests/data/dummy_abci/__init__.py | 28 + .../tests/data/dummy_abci/behaviours.py | 158 + .../tests/data/dummy_abci/dialogues.py | 81 + .../tests/data/dummy_abci/handlers.py | 47 + .../tests/data/dummy_abci/models.py | 44 + .../tests/data/dummy_abci/payloads.py | 53 + .../tests/data/dummy_abci/rounds.py | 152 + .../tests/data/dummy_abci/skill.yaml | 148 + .../tests/test_abci_app_chain.py | 508 +++ .../abstract_round_abci/tests/test_base.py | 3420 +++++++++++++++ .../tests/test_base_rounds.py | 668 +++ .../tests/test_behaviours.py | 951 ++++ .../tests/test_behaviours_utils.py | 2681 ++++++++++++ .../abstract_round_abci/tests/test_common.py | 407 ++ .../tests/test_dialogues.py | 100 + .../tests/test_handlers.py | 596 +++ .../tests/test_io/__init__.py | 20 + .../tests/test_io/test_ipfs.py | 110 + .../tests/test_io/test_load.py | 114 + .../tests/test_io/test_store.py | 104 + .../abstract_round_abci/tests/test_models.py | 901 ++++ .../tests/test_tools/__init__.py | 20 + .../tests/test_tools/base.py | 86 + .../tests/test_tools/test_base.py | 189 + .../tests/test_tools/test_common.py | 183 + .../tests/test_tools/test_integration.py | 143 + .../tests/test_tools/test_rounds.py | 659 +++ .../abstract_round_abci/tests/test_utils.py | 307 ++ .../skills/abstract_round_abci/utils.py | 504 +++ .../ipfs_package_downloader/__init__.py | 25 + .../ipfs_package_downloader/behaviours.py | 215 + .../ipfs_package_downloader/dialogues.py | 61 + .../ipfs_package_downloader/handlers.py | 89 + .../skills/ipfs_package_downloader/models.py | 68 + .../skills/ipfs_package_downloader/skill.yaml | 57 + .../ipfs_package_downloader/utils/__init__.py | 19 + .../ipfs_package_downloader/utils/ipfs.py | 94 + .../ipfs_package_downloader/utils/task.py | 36 + .../market_data_fetcher_abci/__init__.py | 25 + .../market_data_fetcher_abci/behaviours.py | 449 ++ .../market_data_fetcher_abci/dialogues.py | 101 + .../fsm_specification.yaml | 26 + .../market_data_fetcher_abci/handlers.py | 66 + .../skills/market_data_fetcher_abci/models.py | 187 + .../market_data_fetcher_abci/payloads.py | 39 + .../skills/market_data_fetcher_abci/rounds.py | 175 + .../market_data_fetcher_abci/skill.yaml | 165 + .../skills/portfolio_tracker_abci/README.md | 6 + .../skills/portfolio_tracker_abci/__init__.py | 25 + .../portfolio_tracker_abci/behaviours.py | 545 +++ .../portfolio_tracker_abci/dialogues.py | 101 + .../fsm_specification.yaml | 23 + .../skills/portfolio_tracker_abci/handlers.py | 66 + .../skills/portfolio_tracker_abci/models.py | 100 + .../skills/portfolio_tracker_abci/payloads.py | 33 + .../skills/portfolio_tracker_abci/rounds.py | 167 + .../skills/portfolio_tracker_abci/skill.yaml | 158 + .../valory/skills/registration_abci/README.md | 25 + .../skills/registration_abci/__init__.py | 25 + .../skills/registration_abci/behaviours.py | 492 +++ .../skills/registration_abci/dialogues.py | 90 + .../registration_abci/fsm_specification.yaml | 18 + .../skills/registration_abci/handlers.py | 51 + .../valory/skills/registration_abci/models.py | 41 + .../skills/registration_abci/payloads.py | 31 + .../valory/skills/registration_abci/rounds.py | 178 + .../skills/registration_abci/skill.yaml | 151 + .../registration_abci/tests/__init__.py | 20 + .../tests/test_behaviours.py | 644 +++ .../registration_abci/tests/test_dialogues.py | 28 + .../registration_abci/tests/test_handlers.py | 28 + .../registration_abci/tests/test_models.py | 35 + .../registration_abci/tests/test_payloads.py | 46 + .../registration_abci/tests/test_rounds.py | 371 ++ .../valory/skills/reset_pause_abci/README.md | 23 + .../skills/reset_pause_abci/__init__.py | 25 + .../skills/reset_pause_abci/behaviours.py | 99 + .../skills/reset_pause_abci/dialogues.py | 91 + .../reset_pause_abci/fsm_specification.yaml | 19 + .../skills/reset_pause_abci/handlers.py | 51 + .../valory/skills/reset_pause_abci/models.py | 55 + .../skills/reset_pause_abci/payloads.py | 31 + .../valory/skills/reset_pause_abci/rounds.py | 115 + .../valory/skills/reset_pause_abci/skill.yaml | 141 + .../skills/reset_pause_abci/tests/__init__.py | 20 + .../reset_pause_abci/tests/test_behaviours.py | 154 + .../reset_pause_abci/tests/test_dialogues.py | 28 + .../reset_pause_abci/tests/test_handlers.py | 28 + .../reset_pause_abci/tests/test_payloads.py | 34 + .../reset_pause_abci/tests/test_rounds.py | 106 + .../skills/strategy_evaluator_abci/README.md | 6 + .../strategy_evaluator_abci/__init__.py | 25 + .../behaviours/__init__.py | 20 + .../behaviours/backtesting.py | 132 + .../behaviours/base.py | 293 ++ .../behaviours/prepare_swap_tx.py | 412 ++ .../behaviours/proxy_swap_queue.py | 147 + .../behaviours/round_behaviour.py | 61 + .../behaviours/strategy_exec.py | 351 ++ .../behaviours/swap_queue.py | 91 + .../strategy_evaluator_abci/dialogues.py | 100 + .../fsm_specification.yaml | 85 + .../strategy_evaluator_abci/handlers.py | 67 + .../skills/strategy_evaluator_abci/models.py | 148 + .../strategy_evaluator_abci/payloads.py | 55 + .../skills/strategy_evaluator_abci/rounds.py | 208 + .../skills/strategy_evaluator_abci/skill.yaml | 257 ++ .../states/__init__.py | 20 + .../states/backtesting.py | 52 + .../strategy_evaluator_abci/states/base.py | 182 + .../states/final_states.py | 55 + .../states/prepare_swap.py | 59 + .../states/proxy_swap_queue.py | 57 + .../states/strategy_exec.py | 41 + .../states/swap_queue.py | 59 + .../trader_decision_maker_abci/README.md | 5 + .../trader_decision_maker_abci/__init__.py | 25 + .../trader_decision_maker_abci/behaviours.py | 234 + .../trader_decision_maker_abci/dialogues.py | 90 + .../fsm_specification.yaml | 25 + .../trader_decision_maker_abci/handlers.py | 50 + .../trader_decision_maker_abci/models.py | 54 + .../trader_decision_maker_abci/payloads.py | 42 + .../trader_decision_maker_abci/policy.py | 129 + .../trader_decision_maker_abci/rounds.py | 232 + .../trader_decision_maker_abci/skill.yaml | 142 + .../trader_decision_maker_abci/utils.py | 34 + .../transaction_settlement_abci/README.md | 53 + .../transaction_settlement_abci/__init__.py | 25 + .../transaction_settlement_abci/behaviours.py | 984 +++++ .../transaction_settlement_abci/dialogues.py | 91 + .../fsm_specification.yaml | 88 + .../transaction_settlement_abci/handlers.py | 51 + .../transaction_settlement_abci/models.py | 123 + .../payload_tools.py | 183 + .../transaction_settlement_abci/payloads.py | 82 + .../transaction_settlement_abci/rounds.py | 831 ++++ .../transaction_settlement_abci/skill.yaml | 174 + .../test_tools/__init__.py | 20 + .../test_tools/integration.py | 338 ++ .../tests/__init__.py | 24 + .../tests/test_behaviours.py | 1392 ++++++ .../tests/test_dialogues.py | 28 + .../tests/test_handlers.py | 28 + .../tests/test_models.py | 95 + .../tests/test_payload_tools.py | 101 + .../tests/test_payloads.py | 126 + .../tests/test_rounds.py | 1023 +++++ .../tests/test_tools/__init__.py | 20 + .../tests/test_tools/test_integration.py | 214 + 186 files changed, 43238 insertions(+), 1 deletion(-) create mode 100644 packages/valory/agents/optimus/aea-config-optimus.yaml create mode 100644 packages/valory/skills/__init__.py create mode 100644 packages/valory/skills/abstract_abci/README.md create mode 100644 packages/valory/skills/abstract_abci/__init__.py create mode 100644 packages/valory/skills/abstract_abci/dialogues.py create mode 100644 packages/valory/skills/abstract_abci/handlers.py create mode 100644 packages/valory/skills/abstract_abci/skill.yaml create mode 100644 packages/valory/skills/abstract_abci/tests/__init__.py create mode 100644 packages/valory/skills/abstract_abci/tests/test_dialogues.py create mode 100644 packages/valory/skills/abstract_abci/tests/test_handlers.py create mode 100644 packages/valory/skills/abstract_round_abci/README.md create mode 100644 packages/valory/skills/abstract_round_abci/__init__.py create mode 100644 packages/valory/skills/abstract_round_abci/abci_app_chain.py create mode 100644 packages/valory/skills/abstract_round_abci/base.py create mode 100644 packages/valory/skills/abstract_round_abci/behaviour_utils.py create mode 100644 packages/valory/skills/abstract_round_abci/behaviours.py create mode 100644 packages/valory/skills/abstract_round_abci/common.py create mode 100644 packages/valory/skills/abstract_round_abci/dialogues.py create mode 100644 packages/valory/skills/abstract_round_abci/handlers.py create mode 100644 packages/valory/skills/abstract_round_abci/io_/__init__.py create mode 100644 packages/valory/skills/abstract_round_abci/io_/ipfs.py create mode 100644 packages/valory/skills/abstract_round_abci/io_/load.py create mode 100644 packages/valory/skills/abstract_round_abci/io_/paths.py create mode 100644 packages/valory/skills/abstract_round_abci/io_/store.py create mode 100644 packages/valory/skills/abstract_round_abci/models.py create mode 100644 packages/valory/skills/abstract_round_abci/skill.yaml create mode 100644 packages/valory/skills/abstract_round_abci/test_tools/__init__.py create mode 100644 packages/valory/skills/abstract_round_abci/test_tools/abci_app.py create mode 100644 packages/valory/skills/abstract_round_abci/test_tools/base.py create mode 100644 packages/valory/skills/abstract_round_abci/test_tools/common.py create mode 100644 packages/valory/skills/abstract_round_abci/test_tools/integration.py create mode 100644 packages/valory/skills/abstract_round_abci/test_tools/rounds.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/__init__.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/conftest.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/data/__init__.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/__init__.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/behaviours.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/dialogues.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/handlers.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/models.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/payloads.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/rounds.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/skill.yaml create mode 100644 packages/valory/skills/abstract_round_abci/tests/test_abci_app_chain.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/test_base.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/test_base_rounds.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/test_behaviours.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/test_behaviours_utils.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/test_common.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/test_dialogues.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/test_handlers.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/test_io/__init__.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/test_io/test_ipfs.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/test_io/test_load.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/test_io/test_store.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/test_models.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/test_tools/__init__.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/test_tools/base.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/test_tools/test_base.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/test_tools/test_common.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/test_tools/test_integration.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/test_tools/test_rounds.py create mode 100644 packages/valory/skills/abstract_round_abci/tests/test_utils.py create mode 100644 packages/valory/skills/abstract_round_abci/utils.py create mode 100644 packages/valory/skills/ipfs_package_downloader/__init__.py create mode 100644 packages/valory/skills/ipfs_package_downloader/behaviours.py create mode 100644 packages/valory/skills/ipfs_package_downloader/dialogues.py create mode 100644 packages/valory/skills/ipfs_package_downloader/handlers.py create mode 100644 packages/valory/skills/ipfs_package_downloader/models.py create mode 100644 packages/valory/skills/ipfs_package_downloader/skill.yaml create mode 100644 packages/valory/skills/ipfs_package_downloader/utils/__init__.py create mode 100644 packages/valory/skills/ipfs_package_downloader/utils/ipfs.py create mode 100644 packages/valory/skills/ipfs_package_downloader/utils/task.py create mode 100644 packages/valory/skills/market_data_fetcher_abci/__init__.py create mode 100644 packages/valory/skills/market_data_fetcher_abci/behaviours.py create mode 100644 packages/valory/skills/market_data_fetcher_abci/dialogues.py create mode 100644 packages/valory/skills/market_data_fetcher_abci/fsm_specification.yaml create mode 100644 packages/valory/skills/market_data_fetcher_abci/handlers.py create mode 100644 packages/valory/skills/market_data_fetcher_abci/models.py create mode 100644 packages/valory/skills/market_data_fetcher_abci/payloads.py create mode 100644 packages/valory/skills/market_data_fetcher_abci/rounds.py create mode 100644 packages/valory/skills/market_data_fetcher_abci/skill.yaml create mode 100644 packages/valory/skills/portfolio_tracker_abci/README.md create mode 100644 packages/valory/skills/portfolio_tracker_abci/__init__.py create mode 100644 packages/valory/skills/portfolio_tracker_abci/behaviours.py create mode 100644 packages/valory/skills/portfolio_tracker_abci/dialogues.py create mode 100644 packages/valory/skills/portfolio_tracker_abci/fsm_specification.yaml create mode 100644 packages/valory/skills/portfolio_tracker_abci/handlers.py create mode 100644 packages/valory/skills/portfolio_tracker_abci/models.py create mode 100644 packages/valory/skills/portfolio_tracker_abci/payloads.py create mode 100644 packages/valory/skills/portfolio_tracker_abci/rounds.py create mode 100644 packages/valory/skills/portfolio_tracker_abci/skill.yaml create mode 100644 packages/valory/skills/registration_abci/README.md create mode 100644 packages/valory/skills/registration_abci/__init__.py create mode 100644 packages/valory/skills/registration_abci/behaviours.py create mode 100644 packages/valory/skills/registration_abci/dialogues.py create mode 100644 packages/valory/skills/registration_abci/fsm_specification.yaml create mode 100644 packages/valory/skills/registration_abci/handlers.py create mode 100644 packages/valory/skills/registration_abci/models.py create mode 100644 packages/valory/skills/registration_abci/payloads.py create mode 100644 packages/valory/skills/registration_abci/rounds.py create mode 100644 packages/valory/skills/registration_abci/skill.yaml create mode 100644 packages/valory/skills/registration_abci/tests/__init__.py create mode 100644 packages/valory/skills/registration_abci/tests/test_behaviours.py create mode 100644 packages/valory/skills/registration_abci/tests/test_dialogues.py create mode 100644 packages/valory/skills/registration_abci/tests/test_handlers.py create mode 100644 packages/valory/skills/registration_abci/tests/test_models.py create mode 100644 packages/valory/skills/registration_abci/tests/test_payloads.py create mode 100644 packages/valory/skills/registration_abci/tests/test_rounds.py create mode 100644 packages/valory/skills/reset_pause_abci/README.md create mode 100644 packages/valory/skills/reset_pause_abci/__init__.py create mode 100644 packages/valory/skills/reset_pause_abci/behaviours.py create mode 100644 packages/valory/skills/reset_pause_abci/dialogues.py create mode 100644 packages/valory/skills/reset_pause_abci/fsm_specification.yaml create mode 100644 packages/valory/skills/reset_pause_abci/handlers.py create mode 100644 packages/valory/skills/reset_pause_abci/models.py create mode 100644 packages/valory/skills/reset_pause_abci/payloads.py create mode 100644 packages/valory/skills/reset_pause_abci/rounds.py create mode 100644 packages/valory/skills/reset_pause_abci/skill.yaml create mode 100644 packages/valory/skills/reset_pause_abci/tests/__init__.py create mode 100644 packages/valory/skills/reset_pause_abci/tests/test_behaviours.py create mode 100644 packages/valory/skills/reset_pause_abci/tests/test_dialogues.py create mode 100644 packages/valory/skills/reset_pause_abci/tests/test_handlers.py create mode 100644 packages/valory/skills/reset_pause_abci/tests/test_payloads.py create mode 100644 packages/valory/skills/reset_pause_abci/tests/test_rounds.py create mode 100644 packages/valory/skills/strategy_evaluator_abci/README.md create mode 100644 packages/valory/skills/strategy_evaluator_abci/__init__.py create mode 100644 packages/valory/skills/strategy_evaluator_abci/behaviours/__init__.py create mode 100644 packages/valory/skills/strategy_evaluator_abci/behaviours/backtesting.py create mode 100644 packages/valory/skills/strategy_evaluator_abci/behaviours/base.py create mode 100644 packages/valory/skills/strategy_evaluator_abci/behaviours/prepare_swap_tx.py create mode 100644 packages/valory/skills/strategy_evaluator_abci/behaviours/proxy_swap_queue.py create mode 100644 packages/valory/skills/strategy_evaluator_abci/behaviours/round_behaviour.py create mode 100644 packages/valory/skills/strategy_evaluator_abci/behaviours/strategy_exec.py create mode 100644 packages/valory/skills/strategy_evaluator_abci/behaviours/swap_queue.py create mode 100644 packages/valory/skills/strategy_evaluator_abci/dialogues.py create mode 100644 packages/valory/skills/strategy_evaluator_abci/fsm_specification.yaml create mode 100644 packages/valory/skills/strategy_evaluator_abci/handlers.py create mode 100644 packages/valory/skills/strategy_evaluator_abci/models.py create mode 100644 packages/valory/skills/strategy_evaluator_abci/payloads.py create mode 100644 packages/valory/skills/strategy_evaluator_abci/rounds.py create mode 100644 packages/valory/skills/strategy_evaluator_abci/skill.yaml create mode 100644 packages/valory/skills/strategy_evaluator_abci/states/__init__.py create mode 100644 packages/valory/skills/strategy_evaluator_abci/states/backtesting.py create mode 100644 packages/valory/skills/strategy_evaluator_abci/states/base.py create mode 100644 packages/valory/skills/strategy_evaluator_abci/states/final_states.py create mode 100644 packages/valory/skills/strategy_evaluator_abci/states/prepare_swap.py create mode 100644 packages/valory/skills/strategy_evaluator_abci/states/proxy_swap_queue.py create mode 100644 packages/valory/skills/strategy_evaluator_abci/states/strategy_exec.py create mode 100644 packages/valory/skills/strategy_evaluator_abci/states/swap_queue.py create mode 100644 packages/valory/skills/trader_decision_maker_abci/README.md create mode 100644 packages/valory/skills/trader_decision_maker_abci/__init__.py create mode 100644 packages/valory/skills/trader_decision_maker_abci/behaviours.py create mode 100644 packages/valory/skills/trader_decision_maker_abci/dialogues.py create mode 100644 packages/valory/skills/trader_decision_maker_abci/fsm_specification.yaml create mode 100644 packages/valory/skills/trader_decision_maker_abci/handlers.py create mode 100644 packages/valory/skills/trader_decision_maker_abci/models.py create mode 100644 packages/valory/skills/trader_decision_maker_abci/payloads.py create mode 100644 packages/valory/skills/trader_decision_maker_abci/policy.py create mode 100644 packages/valory/skills/trader_decision_maker_abci/rounds.py create mode 100644 packages/valory/skills/trader_decision_maker_abci/skill.yaml create mode 100644 packages/valory/skills/trader_decision_maker_abci/utils.py create mode 100644 packages/valory/skills/transaction_settlement_abci/README.md create mode 100644 packages/valory/skills/transaction_settlement_abci/__init__.py create mode 100644 packages/valory/skills/transaction_settlement_abci/behaviours.py create mode 100644 packages/valory/skills/transaction_settlement_abci/dialogues.py create mode 100644 packages/valory/skills/transaction_settlement_abci/fsm_specification.yaml create mode 100644 packages/valory/skills/transaction_settlement_abci/handlers.py create mode 100644 packages/valory/skills/transaction_settlement_abci/models.py create mode 100644 packages/valory/skills/transaction_settlement_abci/payload_tools.py create mode 100644 packages/valory/skills/transaction_settlement_abci/payloads.py create mode 100644 packages/valory/skills/transaction_settlement_abci/rounds.py create mode 100644 packages/valory/skills/transaction_settlement_abci/skill.yaml create mode 100644 packages/valory/skills/transaction_settlement_abci/test_tools/__init__.py create mode 100644 packages/valory/skills/transaction_settlement_abci/test_tools/integration.py create mode 100644 packages/valory/skills/transaction_settlement_abci/tests/__init__.py create mode 100644 packages/valory/skills/transaction_settlement_abci/tests/test_behaviours.py create mode 100644 packages/valory/skills/transaction_settlement_abci/tests/test_dialogues.py create mode 100644 packages/valory/skills/transaction_settlement_abci/tests/test_handlers.py create mode 100644 packages/valory/skills/transaction_settlement_abci/tests/test_models.py create mode 100644 packages/valory/skills/transaction_settlement_abci/tests/test_payload_tools.py create mode 100644 packages/valory/skills/transaction_settlement_abci/tests/test_payloads.py create mode 100644 packages/valory/skills/transaction_settlement_abci/tests/test_rounds.py create mode 100644 packages/valory/skills/transaction_settlement_abci/tests/test_tools/__init__.py create mode 100644 packages/valory/skills/transaction_settlement_abci/tests/test_tools/test_integration.py diff --git a/.gitignore b/.gitignore index bba2a75..37831d5 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,7 @@ packages/valory/contracts/multisend packages/valory/contracts/service_registry packages/valory/services/* packages/valory/protocols/* -packages/valory/skills/* +!packages/valory/skills/* !packages/valory/agents/optimus !packages/valory/services/optimus diff --git a/packages/valory/agents/optimus/aea-config-optimus.yaml b/packages/valory/agents/optimus/aea-config-optimus.yaml new file mode 100644 index 0000000..bdff5a3 --- /dev/null +++ b/packages/valory/agents/optimus/aea-config-optimus.yaml @@ -0,0 +1,267 @@ +agent_name: solana_trader +author: valory +version: 0.1.0 +license: Apache-2.0 +description: Solana trader agent. +aea_version: '>=1.0.0, <2.0.0' +fingerprint: + __init__.py: bafybeigx5mdvnamsqfum5ut7htok2y5vsnu7lrvms5gfvqi7hmv7sfbo3a + README.md: bafybeibm2adzlongvgzyepiiymb3hxpsjb43qgr7j4uydebjzcpdrwm3om +fingerprint_ignore_patterns: [] +connections: +- eightballer/dcxt:0.1.0:bafybeihes2id6qmqqybvdqfpchu6h3i5mpnhq6lztfxp52twd22iwqxacq +- valory/abci:0.1.0:bafybeiejymu4ul62zx6weoibnlsrfprfpjnplhjefz6sr6izgdr4sajlnu +- valory/http_client:0.23.0:bafybeihi772xgzpqeipp3fhmvpct4y6e6tpjp4sogwqrnf3wqspgeilg4u +- valory/http_server:0.22.0:bafybeihpgu56ovmq4npazdbh6y6ru5i7zuv6wvdglpxavsckyih56smu7m +- valory/ipfs:0.1.0:bafybeiegnapkvkamis47v5ioza2haerrjdzzb23rptpmcydyneas7jc2wm +- valory/ledger:0.19.0:bafybeigntoericenpzvwejqfuc3kqzo2pscs76qoygg5dbj6f4zxusru5e +- valory/p2p_libp2p_client:0.1.0:bafybeid3xg5k2ol5adflqloy75ibgljmol6xsvzvezebsg7oudxeeolz7e +contracts: +- eightballer/erc_20:0.1.0:bafybeiezbnm3f5zhuj5bsc542isnlh2fki5q4nmm2vsajzps4uuoamofo4 +- valory/multisend:0.1.0:bafybeig5byt5urg2d2bsecufxe5ql7f4mezg3mekfleeh32nmuusx66p4y +- valory/service_registry:0.1.0:bafybeihafe524ilngwzavkhwz4er56p7nyar26lfm7lrksfiqvvzo3kdcq +- valory/gnosis_safe:0.1.0:bafybeiho6sbfts3zk3mftrngw37d5qnlvkqtnttt3fzexmcwkeevhu4wwi +- valory/gnosis_safe_proxy_factory:0.1.0:bafybeicpcpyurm7gxir2gnlsgzeirzomkhcbnzr5txk67zdf4mmg737rtu +- valory/balancer_weighted_pool:0.1.0:bafybeidyjlrlq3jrbackewedwt5irokhjupxgpqfgur2ri426cap2oqt7a +- valory/balancer_vault:0.1.0:bafybeie6twptrkqddget7pjijzob2c4jqmrrtpkwombneh35xx56djz4ru +- valory/uniswap_v3_non_fungible_position_manager:0.1.0:bafybeigadr3nyx6tkrual7oqn2qiup35addfevromxjzzlvkiukpyhtz6y +- valory/uniswap_v3_pool:0.1.0:bafybeih64nqgwlverl2tubnkymtlvewngn2pthzzfjewvxpk7dt2im6gza +protocols: +- open_aea/signing:1.0.0:bafybeihv62fim3wl2bayavfcg3u5e5cxu3b7brtu4cn5xoxd6lqwachasi +- valory/abci:0.1.0:bafybeiaqmp7kocbfdboksayeqhkbrynvlfzsx4uy4x6nohywnmaig4an7u +- valory/acn:1.1.0:bafybeidluaoeakae3exseupaea4i3yvvk5vivyt227xshjlffywwxzcxqe +- valory/contract_api:1.0.0:bafybeidgu7o5llh26xp3u3ebq3yluull5lupiyeu6iooi2xyymdrgnzq5i +- valory/http:1.0.0:bafybeifugzl63kfdmwrxwphrnrhj7bn6iruxieme3a4ntzejf6kmtuwmae +- valory/ipfs:0.1.0:bafybeiftxi2qhreewgsc5wevogi7yc5g6hbcbo4uiuaibauhv3nhfcdtvm +- valory/ledger_api:1.0.0:bafybeihdk6psr4guxmbcrc26jr2cbgzpd5aljkqvpwo64bvaz7tdti2oni +- valory/tendermint:0.1.0:bafybeig4mi3vmlv5zpbjbfuzcgida6j5f2nhrpedxicmrrfjweqc5r7cra +skills: +- valory/abstract_abci:0.1.0:bafybeidz54kvxhbdmpruzguuzzq7bjg4pekjb5amqobkxoy4oqknnobopu +- valory/abstract_round_abci:0.1.0:bafybeiajjzuh6vf23crp55humonknirvv2f4s3dmdlfzch6tc5ow52pcgm +- valory/liquidity_trader_abci:0.1.0:bafybeihtca6gtyjibj6wkrcdmx3fb3a3bkpdgsphwevkatagxrbqvh6fd4 +- valory/optimus_abci:0.1.0:bafybeifjpvqz2m7qhztib4xcjpbjkuiutrot22flqclg36amvqvrp5ra3e +- valory/registration_abci:0.1.0:bafybeiffipsowrqrkhjoexem7ern5ob4fabgif7wa6gtlszcoaop2e3oey +- valory/reset_pause_abci:0.1.0:bafybeif4lgvbzsmzljesxbphycdv52ka7qnihyjrjpfaseclxadcmm6yiq +- valory/termination_abci:0.1.0:bafybeiekkpo5qef5zaeagm3si6v45qxcojvtjqe4a5ceccvk4q7k3xi3bi +- valory/transaction_settlement_abci:0.1.0:bafybeielv6eivt2z6nforq43xewl2vmpfwpdu2s2vfogobziljnwsclmlm +- valory/market_data_fetcher_abci:0.1.0:bafybeia3kld7ogbaolbxskys7r5ccolhm53fqi4tdkrwnvilfm7gn5ztcm +- valory/strategy_evaluator_abci:0.1.0:bafybeig2mx3abjgjhiizx2kez3462mcygjwlqj5d6jvnovcf7rwzaql43e +- valory/trader_abci:0.1.0:bafybeiccni66lhpc6nt4hirtakw3xzhera7kqkzmreuznrpuxpckuh455e +- valory/trader_decision_maker_abci:0.1.0:bafybeih7duxvbjevmeez4bvdpahgvefoik3hnpjq3ik5g4jaqbuw5rybtu +- valory/ipfs_package_downloader:0.1.0:bafybeid54srronvfqbvcdjgtuhmr4mbndjkpxtgzguykeg4p3wwj3zboyi +- valory/portfolio_tracker_abci:0.1.0:bafybeigzyhm3fzoxhggjdexryzqgskafoi6rec4ois34n3asodxn6j3txm +default_ledger: ethereum +required_ledgers: +- ethereum +- solana +default_routing: {} +connection_private_key_paths: {} +private_key_paths: {} +logging_config: + version: 1 + disable_existing_loggers: false + formatters: + standard: + format: '[%(asctime)s] [%(levelname)s] %(message)s' + handlers: + logfile: + class: logging.FileHandler + formatter: standard + filename: ${LOG_FILE:str:log.txt} + level: ${LOG_LEVEL:str:INFO} + console: + class: logging.StreamHandler + formatter: standard + stream: ext://sys.stdout + loggers: + aea: + handlers: + - logfile + - console + propagate: true +dependencies: + open-aea-ledger-cosmos: + version: ==1.55.0 + open-aea-ledger-solana: + version: ==1.55.0 + open-aea-ledger-ethereum: + version: ==1.55.0 + open-aea-test-autonomy: + version: ==0.15.2 + pyalgotrade: + version: ==0.20 + open-aea-ledger-ethereum-tool: + version: ==1.57.0 +skill_exception_policy: stop_and_exit +connection_exception_policy: just_log +default_connection: null +--- +public_id: valory/abci:0.1.0 +type: connection +config: + target_skill_id: valory/trader_abci:0.1.0 + host: ${str:localhost} + port: ${int:26658} + use_tendermint: ${bool:false} +--- +public_id: valory/ledger:0.19.0 +type: connection +config: + ledger_apis: + ethereum: + address: ${str:https://base.blockpi.network/v1/rpc/public} + chain_id: ${int:8453} + poa_chain: ${bool:false} + default_gas_price_strategy: ${str:eip1559} + base: + address: ${str:https://virtual.base.rpc.tenderly.co/5d9c013b-879b-4f20-a6cc-e95dee0d109f} + chain_id: ${int:8453} + poa_chain: ${bool:false} + default_gas_price_strategy: ${str:eip1559} + optimism: + address: ${str:https://mainnet.optimism.io} + chain_id: ${int:10} + poa_chain: ${bool:false} + default_gas_price_strategy: ${str:eip1559} +--- +public_id: valory/p2p_libp2p_client:0.1.0 +type: connection +config: + nodes: + - uri: ${str:acn.staging.autonolas.tech:9005} + public_key: ${str:02d3a830c9d6ea1ae91936951430dee11f4662f33118b02190693be835359a9d77} +cert_requests: +- identifier: acn + ledger_id: ethereum + message_format: '{public_key}' + not_after: '2024-01-01' + not_before: '2023-01-01' + public_key: ${str:02d3a830c9d6ea1ae91936951430dee11f4662f33118b02190693be835359a9d77} + save_path: .certs/acn_cosmos_9005.txt +--- +public_id: valory/http_server:0.22.0:bafybeicblltx7ha3ulthg7bzfccuqqyjmihhrvfeztlgrlcoxhr7kf6nbq +type: connection +config: + host: 0.0.0.0 + target_skill_id: valory/trader_abci:0.1.0 +--- +public_id: valory/http_client:0.23.0 +type: connection +config: + host: ${str:127.0.0.1} + port: ${int:8000} + timeout: ${int:1200} +--- +public_id: eightballer/dcxt:0.1.0 +type: connection +config: + target_skill_id: eightballer/chained_dex_app:0.1.0 + exchanges: + - name: ${str:balancer} + key_path: ${str:ethereum_private_key.txt} + ledger_id: ${str:base} + rpc_url: ${str:https://base.blockpi.network/v1/rpc/public} + etherscan_api_key: ${str:YOUR_ETHERSCAN_API_KEY} +--- +public_id: valory/ipfs_package_downloader:0.1.0 +type: skill +models: + params: + args: + cleanup_freq: ${int:50} + timeout_limit: ${int:3} + file_hash_to_id: ${list:[["bafybeic2fpf5ozhkf5jgzmppmfsprqw5ayfx6spgl3owuws464n7mkhpqi",["sma_strategy"]]]} + component_yaml_filename: ${str:component.yaml} + entry_point_key: ${str:entry_point} + callable_keys: ${list:["run_callable","transform_callable","evaluate_callable"]} +--- +public_id: valory/trader_abci:0.1.0 +type: skill +models: + benchmark_tool: + args: + log_dir: ${str:/benchmarks} + get_balance: + args: + api_id: ${str:get_balance} + headers: + Content-Type: ${str:application/json} + method: ${str:POST} + parameters: ${dict:{}} + response_key: ${str:result:value} + response_type: ${str:int} + error_key: ${str:error:message} + error_type: ${str:str} + retries: ${int:5} + url: ${str:https://api.mainnet-beta.solana.com} + token_accounts: + args: + api_id: ${str:token_accounts} + headers: + Content-Type: ${str:application/json} + method: ${str:POST} + parameters: ${dict:{}} + response_key: ${str:result:value} + response_type: ${str:list} + error_key: ${str:error:message} + error_type: ${str:str} + retries: ${int:5} + url: ${str:https://api.mainnet-beta.solana.com} + coingecko: + args: + token_price_endpoint: ${str:https://api.coingecko.com/api/v3/simple/token_price/{asset_platform_id}?contract_addresses={token_address}&vs_currencies=usd} + coin_price_endpoint: ${str:https://api.coingecko.com/api/v3/coins/{token_id}/market_chart?vs_currency=usd&days=1} + api_key: ${str:null} + prices_field: ${str:prices} + requests_per_minute: ${int:5} + credits: ${int:10000} + rate_limited_code: ${int:429} + tx_settlement_proxy: + args: + api_id: ${str:tx_settlement_proxy} + headers: + Content-Type: ${str:application/json} + method: ${str:POST} + parameters: + amount: ${int:100000000} + slippageBps: ${int:5} + resendAmount: ${int:200} + timeoutInMs: ${int:120000} + priorityFee: ${int:5000000} + response_key: ${str:null} + response_type: ${str:dict} + retries: ${int:5} + url: ${str:http://localhost:3000/tx} + params: + args: + setup: + all_participants: ${list:["0x0000000000000000000000000000000000000000"]} + consensus_threshold: ${int:null} + safe_contract_address: ${str:0x0000000000000000000000000000000000000000} + cleanup_history_depth: ${int:1} + cleanup_history_depth_current: ${int:null} + drand_public_key: ${str:868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31} + genesis_config: + genesis_time: ${str:2022-09-26T00:00:00.000000000Z} + chain_id: ${str:chain-c4daS1} + consensus_params: + block: + max_bytes: ${str:22020096} + max_gas: ${str:-1} + time_iota_ms: ${str:1000} + evidence: + max_age_num_blocks: ${str:100000} + max_age_duration: ${str:172800000000000} + max_bytes: ${str:1048576} + validator: + pub_key_types: ${list:["ed25519"]} + version: ${dict:{}} + voting_power: ${str:10} + init_fallback_gas: ${int:0} + keeper_allowed_retries: ${int:3} + keeper_timeout: ${float:30.0} + max_attempts: ${int:10} + reset_tendermint_after: ${int:2} + retry_attempts: ${int:400} + retry_timeout: ${int:3} + request \ No newline at end of file diff --git a/packages/valory/skills/__init__.py b/packages/valory/skills/__init__.py new file mode 100644 index 0000000..9df9b44 --- /dev/null +++ b/packages/valory/skills/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the skills packages authored by Valory AG.""" diff --git a/packages/valory/skills/abstract_abci/README.md b/packages/valory/skills/abstract_abci/README.md new file mode 100644 index 0000000..4a99a11 --- /dev/null +++ b/packages/valory/skills/abstract_abci/README.md @@ -0,0 +1,20 @@ +# Abstract abci + +## Description + +This module contains an abstract ABCI skill template for an AEA. + +## Behaviours + +No behaviours (the skill is purely reactive). + +## Handlers + +* `ABCIHandler` + + This abstract skill provides a template of an ABCI application managed by an + AEA. This abstract Handler replies to ABCI requests with default responses. + In another skill, extend the class and override the request handlers + to implement a custom behaviour. + + diff --git a/packages/valory/skills/abstract_abci/__init__.py b/packages/valory/skills/abstract_abci/__init__.py new file mode 100644 index 0000000..8c9d135 --- /dev/null +++ b/packages/valory/skills/abstract_abci/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains an abstract ABCI skill template for an AEA.""" + +from aea.configurations.base import PublicId + + +PUBLIC_ID = PublicId.from_str("valory/abstract_abci:0.1.0") diff --git a/packages/valory/skills/abstract_abci/dialogues.py b/packages/valory/skills/abstract_abci/dialogues.py new file mode 100644 index 0000000..e38665b --- /dev/null +++ b/packages/valory/skills/abstract_abci/dialogues.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the classes required for dialogue management.""" + +from typing import Any + +from aea.protocols.base import Address, Message +from aea.protocols.dialogue.base import Dialogue as BaseDialogue +from aea.skills.base import Model + +from packages.valory.protocols.abci.dialogues import AbciDialogue as BaseAbciDialogue +from packages.valory.protocols.abci.dialogues import AbciDialogues as BaseAbciDialogues + + +AbciDialogue = BaseAbciDialogue + + +class AbciDialogues(Model, BaseAbciDialogues): + """The dialogues class keeps track of all dialogues.""" + + def __init__(self, **kwargs: Any) -> None: + """ + Initialize dialogues. + + :param kwargs: keyword arguments + """ + Model.__init__(self, **kwargs) + + def role_from_first_message( # pylint: disable=unused-argument + message: Message, receiver_address: Address + ) -> BaseDialogue.Role: + """Infer the role of the agent from an incoming/outgoing first message + + :param message: an incoming/outgoing first message + :param receiver_address: the address of the receiving agent + :return: The role of the agent + """ + return AbciDialogue.Role.CLIENT + + BaseAbciDialogues.__init__( + self, + self_address=str(self.skill_id), + role_from_first_message=role_from_first_message, + ) diff --git a/packages/valory/skills/abstract_abci/handlers.py b/packages/valory/skills/abstract_abci/handlers.py new file mode 100644 index 0000000..93e95e3 --- /dev/null +++ b/packages/valory/skills/abstract_abci/handlers.py @@ -0,0 +1,408 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the handler for the 'abci' skill.""" +from typing import List, cast + +from aea.protocols.base import Message +from aea.skills.base import Handler + +from packages.valory.connections.abci.connection import PUBLIC_ID +from packages.valory.protocols.abci import AbciMessage +from packages.valory.protocols.abci.custom_types import ( + Events, + ProofOps, + Result, + ResultType, + SnapShots, + ValidatorUpdates, +) +from packages.valory.protocols.abci.dialogues import AbciDialogue, AbciDialogues + + +ERROR_CODE = 1 + + +class ABCIHandler(Handler): + """ + Default ABCI handler. + + This abstract skill provides a template of an ABCI application managed by an + AEA. This abstract Handler replies to ABCI requests with default responses. + In another skill, extend the class and override the request handlers + to implement a custom behaviour. + """ + + SUPPORTED_PROTOCOL = AbciMessage.protocol_id + + def setup(self) -> None: + """Set up the handler.""" + self.context.logger.debug( + f"ABCI Handler: setup method called. Using {PUBLIC_ID}." + ) + + def handle(self, message: Message) -> None: + """ + Handle the message. + + :param message: the message. + """ + abci_message = cast(AbciMessage, message) + + # recover dialogue + abci_dialogues = cast(AbciDialogues, self.context.abci_dialogues) + abci_dialogue = cast(AbciDialogue, abci_dialogues.update(message)) + + if abci_dialogue is None: + self.log_exception(abci_message, "Invalid dialogue.") + return + + performative = message.performative.value + + # handle message + request_type = performative.replace("request_", "") + self.context.logger.debug(f"Received ABCI request of type {request_type}") + handler = getattr(self, request_type, None) + if handler is None: # pragma: nocover + self.context.logger.warning( + f"Cannot handle request '{request_type}', ignoring..." + ) + return + + self.context.logger.debug( + "ABCI Handler: message={}, sender={}".format(message, message.sender) + ) + response = handler(message, abci_dialogue) + self.context.outbox.put_message(message=response) + + def teardown(self) -> None: + """Teardown the handler.""" + self.context.logger.debug("ABCI Handler: teardown method called.") + + def log_exception(self, message: AbciMessage, error_message: str) -> None: + """Log a response exception.""" + self.context.logger.error( + f"An exception occurred: {error_message} for message: {message}" + ) + + def echo( # pylint: disable=no-self-use + self, message: AbciMessage, dialogue: AbciDialogue + ) -> AbciMessage: + """ + Handle a message of REQUEST_ECHO performative. + + :param message: the ABCI request. + :param dialogue: the ABCI dialogue. + :return: the response. + """ + reply = dialogue.reply( + performative=AbciMessage.Performative.RESPONSE_ECHO, + target_message=message, + message=message.message, + ) + return cast(AbciMessage, reply) + + def info( # pylint: disable=no-self-use + self, message: AbciMessage, dialogue: AbciDialogue + ) -> AbciMessage: + """ + Handle a message of REQUEST_INFO performative. + + :param message: the ABCI request. + :param dialogue: the ABCI dialogue. + :return: the response. + """ + info_data = "" + version = "" + app_version = 0 + last_block_height = 0 + last_block_app_hash = b"" + reply = dialogue.reply( + performative=AbciMessage.Performative.RESPONSE_INFO, + target_message=message, + info_data=info_data, + version=version, + app_version=app_version, + last_block_height=last_block_height, + last_block_app_hash=last_block_app_hash, + ) + return cast(AbciMessage, reply) + + def flush( # pylint: disable=no-self-use + self, + message: AbciMessage, + dialogue: AbciDialogue, + ) -> AbciMessage: + """ + Handle a message of REQUEST_FLUSH performative. + + :param message: the ABCI request. + :param dialogue: the ABCI dialogue. + :return: the response. + """ + reply = dialogue.reply( + performative=AbciMessage.Performative.RESPONSE_FLUSH, + target_message=message, + ) + return cast(AbciMessage, reply) + + def set_option( # pylint: disable=no-self-use + self, + message: AbciMessage, + dialogue: AbciDialogue, + ) -> AbciMessage: + """ + Handle a message of REQUEST_SET_OPTION performative. + + :param message: the ABCI request. + :param dialogue: the ABCI dialogue. + :return: the response. + """ + reply = dialogue.reply( + performative=AbciMessage.Performative.RESPONSE_SET_OPTION, + target_message=message, + code=ERROR_CODE, + log="operation not supported", + info="operation not supported", + ) + return cast(AbciMessage, reply) + + def init_chain( # pylint: disable=no-self-use + self, message: AbciMessage, dialogue: AbciDialogue + ) -> AbciMessage: + """ + Handle a message of REQUEST_INIT_CHAIN performative. + + :param message: the ABCI request. + :param dialogue: the ABCI dialogue. + :return: the response. + """ + validators: List = [] + app_hash = b"" + reply = dialogue.reply( + performative=AbciMessage.Performative.RESPONSE_INIT_CHAIN, + target_message=message, + validators=ValidatorUpdates(validators), + app_hash=app_hash, + ) + return cast(AbciMessage, reply) + + def query( # pylint: disable=no-self-use + self, message: AbciMessage, dialogue: AbciDialogue + ) -> AbciMessage: + """ + Handle a message of REQUEST_QUERY performative. + + :param message: the ABCI request. + :param dialogue: the ABCI dialogue. + :return: the response. + """ + reply = dialogue.reply( + performative=AbciMessage.Performative.RESPONSE_QUERY, + target_message=message, + code=ERROR_CODE, + log="operation not supported", + info="operation not supported", + index=0, + key=b"", + value=b"", + proof_ops=ProofOps([]), + height=0, + codespace="", + ) + return cast(AbciMessage, reply) + + def check_tx( # pylint: disable=no-self-use + self, message: AbciMessage, dialogue: AbciDialogue + ) -> AbciMessage: + """ + Handle a message of REQUEST_CHECK_TX performative. + + :param message: the ABCI request. + :param dialogue: the ABCI dialogue. + :return: the response. + """ + reply = dialogue.reply( + performative=AbciMessage.Performative.RESPONSE_CHECK_TX, + target_message=message, + code=ERROR_CODE, + data=b"", + log="operation not supported", + info="operation not supported", + gas_wanted=0, + gas_used=0, + events=Events([]), + codespace="", + ) + return cast(AbciMessage, reply) + + def deliver_tx( # pylint: disable=no-self-use + self, message: AbciMessage, dialogue: AbciDialogue + ) -> AbciMessage: + """ + Handle a message of REQUEST_DELIVER_TX performative. + + :param message: the ABCI request. + :param dialogue: the ABCI dialogue. + :return: the response. + """ + reply = dialogue.reply( + performative=AbciMessage.Performative.RESPONSE_DELIVER_TX, + target_message=message, + code=ERROR_CODE, + data=b"", + log="operation not supported", + info="operation not supported", + gas_wanted=0, + gas_used=0, + events=Events([]), + codespace="", + ) + return cast(AbciMessage, reply) + + def begin_block( # pylint: disable=no-self-use + self, message: AbciMessage, dialogue: AbciDialogue + ) -> AbciMessage: + """ + Handle a message of REQUEST_BEGIN_BLOCK performative. + + :param message: the ABCI request. + :param dialogue: the ABCI dialogue. + :return: the response. + """ + reply = dialogue.reply( + performative=AbciMessage.Performative.RESPONSE_BEGIN_BLOCK, + target_message=message, + events=Events([]), + ) + return cast(AbciMessage, reply) + + def end_block( # pylint: disable=no-self-use + self, message: AbciMessage, dialogue: AbciDialogue + ) -> AbciMessage: + """ + Handle a message of REQUEST_END_BLOCK performative. + + :param message: the ABCI request. + :param dialogue: the ABCI dialogue. + :return: the response. + """ + reply = dialogue.reply( + performative=AbciMessage.Performative.RESPONSE_END_BLOCK, + target_message=message, + validator_updates=ValidatorUpdates([]), + events=Events([]), + ) + return cast(AbciMessage, reply) + + def commit( # pylint: disable=no-self-use + self, message: AbciMessage, dialogue: AbciDialogue + ) -> AbciMessage: + """ + Handle a message of REQUEST_COMMIT performative. + + :param message: the ABCI request. + :param dialogue: the ABCI dialogue. + :return: the response. + """ + reply = dialogue.reply( + performative=AbciMessage.Performative.RESPONSE_COMMIT, + target_message=message, + data=b"", + retain_height=0, + ) + return cast(AbciMessage, reply) + + def list_snapshots( # pylint: disable=no-self-use + self, + message: AbciMessage, + dialogue: AbciDialogue, + ) -> AbciMessage: + """ + Handle a message of REQUEST_LIST_SNAPSHOT performative. + + :param message: the ABCI request. + :param dialogue: the ABCI dialogue. + :return: the response. + """ + reply = dialogue.reply( + performative=AbciMessage.Performative.RESPONSE_LIST_SNAPSHOTS, + target_message=message, + snapshots=SnapShots([]), + ) + return cast(AbciMessage, reply) + + def offer_snapshot( # pylint: disable=no-self-use + self, + message: AbciMessage, + dialogue: AbciDialogue, + ) -> AbciMessage: + """ + Handle a message of REQUEST_OFFER_SNAPSHOT performative. + + :param message: the ABCI request. + :param dialogue: the ABCI dialogue. + :return: the response. + """ + reply = dialogue.reply( + performative=AbciMessage.Performative.RESPONSE_OFFER_SNAPSHOT, + target_message=message, + result=Result(ResultType.REJECT), # by default, we reject + ) + return cast(AbciMessage, reply) + + def load_snapshot_chunk( # pylint: disable=no-self-use + self, + message: AbciMessage, + dialogue: AbciDialogue, + ) -> AbciMessage: + """ + Handle a message of REQUEST_LOAD_SNAPSHOT_CHUNK performative. + + :param message: the ABCI request. + :param dialogue: the ABCI dialogue. + :return: the response. + """ + reply = dialogue.reply( + performative=AbciMessage.Performative.RESPONSE_LOAD_SNAPSHOT_CHUNK, + target_message=message, + chunk=b"", + ) + return cast(AbciMessage, reply) + + def apply_snapshot_chunk( # pylint: disable=no-self-use + self, + message: AbciMessage, + dialogue: AbciDialogue, + ) -> AbciMessage: + """ + Handle a message of REQUEST_APPLY_SNAPSHOT_CHUNK performative. + + :param message: the ABCI request. + :param dialogue: the ABCI dialogue. + :return: the response. + """ + reply = dialogue.reply( + performative=AbciMessage.Performative.RESPONSE_APPLY_SNAPSHOT_CHUNK, + target_message=message, + result=Result(ResultType.REJECT), + refetch_chunks=tuple(), + reject_senders=tuple(), + ) + return cast(AbciMessage, reply) diff --git a/packages/valory/skills/abstract_abci/skill.yaml b/packages/valory/skills/abstract_abci/skill.yaml new file mode 100644 index 0000000..6811750 --- /dev/null +++ b/packages/valory/skills/abstract_abci/skill.yaml @@ -0,0 +1,34 @@ +name: abstract_abci +author: valory +version: 0.1.0 +type: skill +description: The abci skill provides a template of an ABCI application. +license: Apache-2.0 +aea_version: '>=1.0.0, <2.0.0' +fingerprint: + README.md: bafybeiezmhsokdhxat2gzxgau2zotd5nqjepg5lb2y7ypijuuq75xnxxrq + __init__.py: bafybeigdpqcsxpxp3akxdy5wcccfahom7pmbrnmututws2fmpcr7q6ryoe + dialogues.py: bafybeib6cex55nl57xe6boa4c3z4ynlxstnospqjehdb5owpgtvzsu5ucm + handlers.py: bafybeihb25swvt26vtqpnvldf6viizt34ophj6hijfpu5pevrlmvpvzkdq + tests/__init__.py: bafybeicnx4gezk2zrgz23mco2kv7ws3yd5yspku5e3ng4cb5tw7s2zexsu + tests/test_dialogues.py: bafybeig3kubiyq7bqmetrka67fjk7vymgtjwguyui3yubbvgtzzhfizsdu + tests/test_handlers.py: bafybeieeuwtu35ddaevr2wgnk33l7kdhrx7ruoeb5jiltiyn65ufdcnopu +fingerprint_ignore_patterns: [] +connections: +- valory/abci:0.1.0:bafybeie4eixvrdpc5ifoovj24a6res6g2e22dl6di6gzib7d3fczshzyti +contracts: [] +protocols: +- valory/abci:0.1.0:bafybeiaqmp7kocbfdboksayeqhkbrynvlfzsx4uy4x6nohywnmaig4an7u +skills: [] +behaviours: {} +handlers: + abci: + args: {} + class_name: ABCIHandler +models: + abci_dialogues: + args: {} + class_name: AbciDialogues +dependencies: {} +is_abstract: true +customs: [] diff --git a/packages/valory/skills/abstract_abci/tests/__init__.py b/packages/valory/skills/abstract_abci/tests/__init__.py new file mode 100644 index 0000000..499994e --- /dev/null +++ b/packages/valory/skills/abstract_abci/tests/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Tests for valory/abstract_abci skill.""" diff --git a/packages/valory/skills/abstract_abci/tests/test_dialogues.py b/packages/valory/skills/abstract_abci/tests/test_dialogues.py new file mode 100644 index 0000000..4de0596 --- /dev/null +++ b/packages/valory/skills/abstract_abci/tests/test_dialogues.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test the dialogues.py module of the skill.""" +from enum import Enum +from typing import Type, cast +from unittest.mock import MagicMock + +import pytest +from aea.protocols.dialogue.base import Dialogues + +from packages.valory.skills.abstract_abci.dialogues import AbciDialogue, AbciDialogues + + +@pytest.mark.parametrize( + "dialogues_cls,expected_role_from_first_message", + [ + (AbciDialogues, AbciDialogue.Role.CLIENT), + ], +) +def test_dialogues_creation( + dialogues_cls: Type[AbciDialogues], expected_role_from_first_message: Enum +) -> None: + """Test XDialogues creations.""" + dialogues = cast(Dialogues, dialogues_cls(name="", skill_context=MagicMock())) + assert ( + expected_role_from_first_message + == dialogues._role_from_first_message( # pylint: disable=protected-access + MagicMock(), MagicMock() + ) + ) diff --git a/packages/valory/skills/abstract_abci/tests/test_handlers.py b/packages/valory/skills/abstract_abci/tests/test_handlers.py new file mode 100644 index 0000000..c653c34 --- /dev/null +++ b/packages/valory/skills/abstract_abci/tests/test_handlers.py @@ -0,0 +1,398 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test the handlers.py module of the skill.""" +import logging +from pathlib import Path +from typing import Any, cast +from unittest.mock import MagicMock, patch + +from aea.configurations.data_types import PublicId +from aea.protocols.base import Address, Message +from aea.protocols.dialogue.base import Dialogue as BaseDialogue +from aea.test_tools.test_skill import BaseSkillTestCase + +from packages.valory.connections.abci.connection import PUBLIC_ID +from packages.valory.protocols.abci import AbciMessage +from packages.valory.protocols.abci.custom_types import ( + CheckTxType, + CheckTxTypeEnum, + Evidences, + Header, + LastCommitInfo, + PublicKey, + Result, + ResultType, + SnapShots, + Snapshot, + Timestamp, + ValidatorUpdate, + ValidatorUpdates, +) +from packages.valory.protocols.abci.dialogues import AbciDialogues as BaseAbciDialogues +from packages.valory.skills.abstract_abci.dialogues import AbciDialogue, AbciDialogues +from packages.valory.skills.abstract_abci.handlers import ABCIHandler, ERROR_CODE + + +PACKAGE_DIR = Path(__file__).parent.parent + + +class AbciDialoguesServer(BaseAbciDialogues): + """The dialogues class keeps track of all ABCI dialogues.""" + + def __init__(self, address: str) -> None: + """Initialize dialogues.""" + self.address = address + + def role_from_first_message( # pylint: disable=unused-argument + message: Message, receiver_address: Address + ) -> BaseDialogue.Role: + """Infer the role of the agent from an incoming/outgoing first message + + :param message: an incoming/outgoing first message + :param receiver_address: the address of the receiving agent + :return: The role of the agent + """ + return AbciDialogue.Role.SERVER + + BaseAbciDialogues.__init__( + self, + self_address=self.address, + role_from_first_message=role_from_first_message, + dialogue_class=AbciDialogue, + ) + + +class TestABCIHandlerOld(BaseSkillTestCase): + """Test ABCIHandler methods.""" + + path_to_skill = PACKAGE_DIR + abci_handler: ABCIHandler + logger: logging.Logger + abci_dialogues: AbciDialogues + + @classmethod + def setup_class(cls, **kwargs: Any) -> None: + """Setup the test class.""" + super().setup_class() + cls.abci_handler = cast(ABCIHandler, cls._skill.skill_context.handlers.abci) + cls.logger = cls._skill.skill_context.logger + + cls.abci_dialogues = cast( + AbciDialogues, cls._skill.skill_context.abci_dialogues + ) + + def test_setup(self) -> None: + """Test the setup method of the echo handler.""" + with patch.object(self.logger, "log") as mock_logger: + self.abci_handler.setup() + + # after + self.assert_quantity_in_outbox(0) + + mock_logger.assert_any_call( + logging.DEBUG, f"ABCI Handler: setup method called. Using {PUBLIC_ID}." + ) + + def test_teardown(self) -> None: + """Test the teardown method of the echo handler.""" + with patch.object(self.logger, "log") as mock_logger: + self.abci_handler.teardown() + + # after + self.assert_quantity_in_outbox(0) + + mock_logger.assert_any_call( + logging.DEBUG, "ABCI Handler: teardown method called." + ) + + +class TestABCIHandler: + """Test 'ABCIHandler'.""" + + def setup(self) -> None: + """Set up the tests.""" + self.skill_id = ( # pylint: disable=attribute-defined-outside-init + PublicId.from_str("dummy/skill:0.1.0") + ) + self.context = MagicMock( # pylint: disable=attribute-defined-outside-init + skill_id=self.skill_id + ) + self.context.abci_dialogues = AbciDialogues(name="", skill_context=self.context) + self.dialogues = ( # pylint: disable=attribute-defined-outside-init + AbciDialoguesServer(address="server") + ) + self.handler = ABCIHandler( # pylint: disable=attribute-defined-outside-init + name="", skill_context=self.context + ) + + def test_setup(self) -> None: + """Test the setup method.""" + self.handler.setup() + + def test_teardown(self) -> None: + """Test the teardown method.""" + self.handler.teardown() + + def test_handle(self) -> None: + """Test the message gets handled.""" + message, _ = self.dialogues.create( + counterparty=str(self.skill_id), + performative=AbciMessage.Performative.REQUEST_INFO, + version="", + block_version=0, + p2p_version=0, + ) + self.handler.handle(cast(AbciMessage, message)) + + def test_handle_log_exception(self) -> None: + """Test the message gets handled.""" + message = AbciMessage( + dialogue_reference=("", ""), + performative=AbciMessage.Performative.REQUEST_INFO, # type: ignore + version="", + block_version=0, + p2p_version=0, + target=0, + message_id=1, + ) + message._sender = "server" # pylint: disable=protected-access + message._to = str(self.skill_id) # pylint: disable=protected-access + self.handler.handle(message) + + def test_info(self) -> None: + """Test the 'info' handler method.""" + message, dialogue = self.dialogues.create( + counterparty="", + performative=AbciMessage.Performative.REQUEST_INFO, + version="", + block_version=0, + p2p_version=0, + ) + response = self.handler.info( + cast(AbciMessage, message), cast(AbciDialogue, dialogue) + ) + assert response.performative == AbciMessage.Performative.RESPONSE_INFO + + def test_echo(self) -> None: + """Test the 'echo' handler method.""" + expected_message = "message" + message, dialogue = self.dialogues.create( + counterparty="", + performative=AbciMessage.Performative.REQUEST_ECHO, + message=expected_message, + ) + response = self.handler.echo( + cast(AbciMessage, message), cast(AbciDialogue, dialogue) + ) + assert response.performative == AbciMessage.Performative.RESPONSE_ECHO + assert response.message == expected_message + + def test_set_option(self) -> None: + """Test the 'set_option' handler method.""" + message, dialogue = self.dialogues.create( + counterparty="", + performative=AbciMessage.Performative.REQUEST_SET_OPTION, + ) + response = self.handler.set_option( + cast(AbciMessage, message), cast(AbciDialogue, dialogue) + ) + assert response.performative == AbciMessage.Performative.RESPONSE_SET_OPTION + assert response.code == ERROR_CODE + + def test_begin_block(self) -> None: + """Test the 'begin_block' handler method.""" + header = Header(*(MagicMock() for _ in range(14))) + last_commit_info = LastCommitInfo(*(MagicMock() for _ in range(2))) + byzantine_validators = Evidences(MagicMock()) + message, dialogue = self.dialogues.create( + counterparty="", + performative=AbciMessage.Performative.REQUEST_BEGIN_BLOCK, + hash=b"", + header=header, + last_commit_info=last_commit_info, + byzantine_validators=byzantine_validators, + ) + response = self.handler.begin_block( + cast(AbciMessage, message), cast(AbciDialogue, dialogue) + ) + assert response.performative == AbciMessage.Performative.RESPONSE_BEGIN_BLOCK + + def test_check_tx(self, *_: Any) -> None: + """Test the 'check_tx' handler method.""" + message, dialogue = self.dialogues.create( + counterparty="", + performative=AbciMessage.Performative.REQUEST_CHECK_TX, + tx=b"", + type=CheckTxType(CheckTxTypeEnum.NEW), + ) + response = self.handler.check_tx( + cast(AbciMessage, message), cast(AbciDialogue, dialogue) + ) + assert response.performative == AbciMessage.Performative.RESPONSE_CHECK_TX + assert response.code == ERROR_CODE + + def test_deliver_tx(self, *_: Any) -> None: + """Test the 'deliver_tx' handler method.""" + message, dialogue = self.dialogues.create( + counterparty="", + performative=AbciMessage.Performative.REQUEST_DELIVER_TX, + tx=b"", + ) + response = self.handler.deliver_tx( + cast(AbciMessage, message), cast(AbciDialogue, dialogue) + ) + assert response.performative == AbciMessage.Performative.RESPONSE_DELIVER_TX + assert response.code == ERROR_CODE + + def test_end_block(self) -> None: + """Test the 'end_block' handler method.""" + message, dialogue = self.dialogues.create( + counterparty="", + performative=AbciMessage.Performative.REQUEST_END_BLOCK, + height=1, + ) + response = self.handler.end_block( + cast(AbciMessage, message), cast(AbciDialogue, dialogue) + ) + assert response.performative == AbciMessage.Performative.RESPONSE_END_BLOCK + + def test_commit(self) -> None: + """Test the 'commit' handler method.""" + message, dialogue = self.dialogues.create( + counterparty="", + performative=AbciMessage.Performative.REQUEST_COMMIT, + ) + response = self.handler.commit( + cast(AbciMessage, message), cast(AbciDialogue, dialogue) + ) + assert response.performative == AbciMessage.Performative.RESPONSE_COMMIT + + def test_flush(self) -> None: + """Test the 'flush' handler method.""" + message, dialogue = self.dialogues.create( + counterparty="", + performative=AbciMessage.Performative.REQUEST_FLUSH, + ) + response = self.handler.flush( + cast(AbciMessage, message), cast(AbciDialogue, dialogue) + ) + assert response.performative == AbciMessage.Performative.RESPONSE_FLUSH + + def test_init_chain(self) -> None: + """Test the 'init_chain' handler method.""" + message, dialogue = self.dialogues.create( + counterparty="", + performative=AbciMessage.Performative.REQUEST_INIT_CHAIN, + time=Timestamp(1, 1), + chain_id="", + validators=ValidatorUpdates( + [ + ValidatorUpdate( + PublicKey(data=b"", key_type=PublicKey.PublicKeyType.ed25519), 1 + ) + ] + ), + app_state_bytes=b"", + initial_height=0, + ) + response = self.handler.init_chain( + cast(AbciMessage, message), cast(AbciDialogue, dialogue) + ) + assert response.performative == AbciMessage.Performative.RESPONSE_INIT_CHAIN + + def test_query(self) -> None: + """Test the 'init_chain' handler method.""" + message, dialogue = self.dialogues.create( + counterparty="", + performative=AbciMessage.Performative.REQUEST_QUERY, + query_data=b"", + path="", + height=0, + prove=True, + ) + response = self.handler.query( + cast(AbciMessage, message), cast(AbciDialogue, dialogue) + ) + assert response.performative == AbciMessage.Performative.RESPONSE_QUERY + assert response.code == ERROR_CODE + + def test_list_snapshots(self) -> None: + """Test the 'list_snapshots' handler method.""" + message, dialogue = self.dialogues.create( + counterparty="", + performative=AbciMessage.Performative.REQUEST_LIST_SNAPSHOTS, + ) + response = self.handler.list_snapshots( + cast(AbciMessage, message), cast(AbciDialogue, dialogue) + ) + assert response.performative == AbciMessage.Performative.RESPONSE_LIST_SNAPSHOTS + assert response.snapshots == SnapShots([]) + + def test_offer_snapshot(self) -> None: + """Test the 'offer_snapshot' handler method.""" + message, dialogue = self.dialogues.create( + counterparty="", + performative=AbciMessage.Performative.REQUEST_OFFER_SNAPSHOT, + snapshot=Snapshot(0, 0, 0, b"", b""), + app_hash=b"", + ) + response = self.handler.offer_snapshot( + cast(AbciMessage, message), cast(AbciDialogue, dialogue) + ) + assert response.performative == AbciMessage.Performative.RESPONSE_OFFER_SNAPSHOT + assert response.result == Result(ResultType.REJECT) + + def test_load_snapshot_chunk(self) -> None: + """Test the 'load_snapshot_chunk' handler method.""" + message, dialogue = self.dialogues.create( + counterparty="", + performative=AbciMessage.Performative.REQUEST_LOAD_SNAPSHOT_CHUNK, + height=0, + format=0, + chunk_index=0, + ) + response = self.handler.load_snapshot_chunk( + cast(AbciMessage, message), cast(AbciDialogue, dialogue) + ) + assert ( + response.performative + == AbciMessage.Performative.RESPONSE_LOAD_SNAPSHOT_CHUNK + ) + assert response.chunk == b"" + + def test_apply_snapshot_chunk(self) -> None: + """Test the 'apply_snapshot_chunk' handler method.""" + message, dialogue = self.dialogues.create( + counterparty="", + performative=AbciMessage.Performative.REQUEST_APPLY_SNAPSHOT_CHUNK, + index=0, + chunk=b"", + chunk_sender="", + ) + response = self.handler.apply_snapshot_chunk( + cast(AbciMessage, message), cast(AbciDialogue, dialogue) + ) + assert ( + response.performative + == AbciMessage.Performative.RESPONSE_APPLY_SNAPSHOT_CHUNK + ) + assert response.result == Result(ResultType.REJECT) + assert response.refetch_chunks == tuple() + assert response.reject_senders == tuple() diff --git a/packages/valory/skills/abstract_round_abci/README.md b/packages/valory/skills/abstract_round_abci/README.md new file mode 100644 index 0000000..4ee033e --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/README.md @@ -0,0 +1,46 @@ +# Abstract round abci + +## Description + +This module contains an abstract round ABCI skill template for an AEA. + +## Behaviours + +* `AbstractRoundBehaviour` + + This behaviour implements an abstract round behaviour. + +* `_MetaRoundBehaviour` + + A metaclass that validates AbstractRoundBehaviour's attributes. + + +## Handlers + +* `ABCIRoundHandler` + + ABCI handler. + +* `AbstractResponseHandler` + + The concrete classes must set the `allowed_response_performatives` + class attribute to the (frozen)set of performative the developer + wants the handler to handle. + +* `ContractApiHandler` + + Implement the contract api handler. + +* `HttpHandler` + + The HTTP response handler. + +* `LedgerApiHandler` + + Implement the ledger handler. + +* `SigningHandler` + + Implement the transaction handler. + + diff --git a/packages/valory/skills/abstract_round_abci/__init__.py b/packages/valory/skills/abstract_round_abci/__init__.py new file mode 100644 index 0000000..2bc8fd5 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains an abstract round ABCI skill template for an AEA.""" # pragma: nocover + +from aea.configurations.base import PublicId # pragma: nocover + + +PUBLIC_ID = PublicId.from_str("valory/abstract_round_abci:0.1.0") diff --git a/packages/valory/skills/abstract_round_abci/abci_app_chain.py b/packages/valory/skills/abstract_round_abci/abci_app_chain.py new file mode 100644 index 0000000..577fb8c --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/abci_app_chain.py @@ -0,0 +1,291 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains utilities for AbciApps.""" +import logging +from copy import deepcopy +from typing import Any, Dict, FrozenSet, List, Optional, Set, Tuple, Type + +from aea.exceptions import enforce + +from packages.valory.skills.abstract_round_abci.base import ( + AbciApp, + AbciAppTransitionFunction, + AppState, + EventToTimeout, + EventType, +) + + +_default_logger = logging.getLogger( + "aea.packages.valory.skills.abstract_round_abci.abci_app_chain" +) + +AbciAppTransitionMapping = Dict[AppState, AppState] + + +def check_set_uniqueness(sets: Tuple) -> Optional[Any]: + """Checks that all elements in the set list are unique and not repeated among different sets""" + all_elements = set.union(*sets) + for element in all_elements: + # Count the number of sets that include this element + sets_in = [set_ for set_ in sets if element in set_] + if len(sets_in) > 1: + return element + return None + + +def chain( # pylint: disable=too-many-locals,too-many-statements + abci_apps: Tuple[Type[AbciApp], ...], + abci_app_transition_mapping: AbciAppTransitionMapping, +) -> Type[AbciApp]: + """ + Concatenate multiple AbciApp types. + + The consistency checks assume that the first element in + abci_apps is the entry-point abci_app (i.e. the associated round of + the initial_behaviour_cls of the AbstractRoundBehaviour in which + the chained AbciApp is used is one of the initial_states of the first element.) + """ + enforce( + len(abci_apps) > 1, + f"there must be a minimum of two AbciApps to chain, found ({len(abci_apps)})", + ) + enforce( + len(set(abci_apps)) == len(abci_apps), + "Found multiple occurrences of same Abci App", + ) + non_abstract_abci_apps = [ + abci_app.__name__ for abci_app in abci_apps if not abci_app.is_abstract() + ] + enforce( + len(non_abstract_abci_apps) == 0, + f"found non-abstract AbciApp during chaining: {non_abstract_abci_apps}", + ) + + # Get the apps rounds + rounds = tuple(app.get_all_rounds() for app in abci_apps) + round_ids = tuple( + {round_.auto_round_id() for round_ in app.get_all_rounds()} for app in abci_apps + ) + + # Ensure there are no common rounds + common_round_classes = check_set_uniqueness(rounds) + enforce( + not common_round_classes, + f"rounds in common between abci apps are not allowed ({common_round_classes})", + ) + + # Ensure there are no common round_ids + common_round_ids = check_set_uniqueness(round_ids) + enforce( + not common_round_ids, + f"round ids in common between abci apps are not allowed ({common_round_ids})", + ) + + # Ensure all states in app transition mapping (keys and values) are final states or initial states, respectively. + all_final_states = { + final_state for app in abci_apps for final_state in app.final_states + } + all_initial_states = { + initial_state for app in abci_apps for initial_state in app.initial_states + }.union({app.initial_round_cls for app in abci_apps}) + for key, value in abci_app_transition_mapping.items(): + if key not in all_final_states: + raise ValueError( + f"Found non-final state {key} specified in abci_app_transition_mapping." + ) + if value not in all_initial_states: + raise ValueError( + f"Found non-initial state {value} specified in abci_app_transition_mapping." + ) + + # Ensure all DB pre- and post-conditions are consistent + # Since we know which app is the "entry-point" we can + # simply work forward from there through all branches. When + # we loop back on an earlier node we stop. + initial_state_to_app: Dict[AppState, Type[AbciApp]] = {} + for value in abci_app_transition_mapping.values(): + for app in abci_apps: + if value in app.initial_states or value == app.initial_round_cls: + initial_state_to_app[value] = app + break + + def get_paths( + initial_state: AppState, + app: Type[AbciApp], + previous_apps: Optional[List[Type[AbciApp]]] = None, + ) -> List[List[Tuple[AppState, Type[AbciApp], Optional[AppState]]]]: + """Get paths.""" + previous_apps_: List[Type[AbciApp]] = ( + deepcopy(previous_apps) if previous_apps is not None else [] + ) + default: List[List[Tuple[AppState, Type[AbciApp], Optional[AppState]]]] = [ + [(initial_state, app, None)] + ] + if app.final_states == {}: + return default # pragma: no cover + paths: List[List[Tuple[AppState, Type[AbciApp], Optional[AppState]]]] = [] + for final_state in app.final_states: + element: Tuple[AppState, Type[AbciApp], Optional[AppState]] = ( + initial_state, + app, + final_state, + ) + if final_state not in abci_app_transition_mapping: + # no linkage defined + paths.append([element]) + continue + next_initial_state = abci_app_transition_mapping[final_state] + next_app = initial_state_to_app[next_initial_state] + if next_app in previous_apps_: + # self-loops do not require attention + # we don't append to path + continue + new_previous_apps = previous_apps_ + [app] + for path in get_paths(next_initial_state, next_app, new_previous_apps): + # if element not in path: + paths.append([element] + path) + return paths if paths else default + + all_paths: List[ + List[Tuple[AppState, Type[AbciApp], Optional[AppState]]] + ] = get_paths(abci_apps[0].initial_round_cls, abci_apps[0]) + new_db_post_conditions: Dict[AppState, Set[str]] = {} + for path in all_paths: + current_initial_state, current_app, current_final_state = path[0] + accumulated_post_conditions: Set[str] = current_app.db_pre_conditions.get( + current_initial_state, set() + ) + for next_initial_state, next_app, next_final_state in path[1:]: + if current_final_state is None: + # No outwards transition, nothing to check. + # we are at the end of a path where the last + # app has no final state and therefore no post conditions + break # pragma: no cover + accumulated_post_conditions = accumulated_post_conditions.union( + set(current_app.db_post_conditions[current_final_state]) + ) + # we now check that the pre conditions of the next app + # are compatible with the post conditions of the current apps. + if next_initial_state in next_app.db_pre_conditions: + diff = set.difference( + next_app.db_pre_conditions[next_initial_state], + accumulated_post_conditions, + ) + if len(diff) != 0: + raise ValueError( + f"Pre conditions '{diff}' of app '{next_app}' not a post condition of app '{current_app}' or any preceding app in path {path}." + ) + else: + raise ValueError( + f"No pre-conditions have been set for {next_initial_state}! " + f"You need to explicitly specify them as empty if there are no pre-conditions for this FSM." + ) + current_app = next_app + current_final_state = next_final_state + + if current_final_state is not None: + new_db_post_conditions[current_final_state] = accumulated_post_conditions + + # Warn about events duplicated in multiple apps + app_to_events = {app: app.get_all_events() for app in abci_apps} + all_events = set.union(*app_to_events.values()) + for event in all_events: + apps = [str(app) for app, events in app_to_events.items() if event in events] + if len(apps) > 1: + apps_str = "\n".join(apps) + _default_logger.warning( + f"The same event '{event}' has been found in several apps:\n{apps_str}\n" + "It will be interpreted as the same event. " + "If this is not the intended behaviour, please rename it to enforce its uniqueness." + ) + + new_initial_round_cls = abci_apps[0].initial_round_cls + new_initial_states = abci_apps[0].initial_states + new_db_pre_conditions = abci_apps[0].db_pre_conditions + + # Merge the transition functions, final states and events + potential_final_states = set.union(*(app.final_states for app in abci_apps)) + potential_events_to_timeout: EventToTimeout = {} + for app in abci_apps: + for e, t in app.event_to_timeout.items(): + if e in potential_events_to_timeout and potential_events_to_timeout[e] != t: + raise ValueError( + f"Event {e} defined in app {app} is defined with timeout {t} but it is already defined in a prior app with timeout {potential_events_to_timeout[e]}." + ) + potential_events_to_timeout[e] = t + + potential_transition_function: AbciAppTransitionFunction = {} + for app in abci_apps: + for state, events_to_rounds in app.transition_function.items(): + if state in abci_app_transition_mapping: + # we remove these final states + continue + # Update transition function according to the transition mapping + new_events_to_rounds = {} + for event, round_ in events_to_rounds.items(): + destination_round = abci_app_transition_mapping.get(round_, round_) + new_events_to_rounds[event] = destination_round + potential_transition_function[state] = new_events_to_rounds + + # Remove no longer used states from transition function and final states + destination_states: Set[AppState] = set() + for event_to_states in potential_transition_function.values(): + destination_states.update(event_to_states.values()) + new_transition_function: AbciAppTransitionFunction = { + state: events_to_rounds + for state, events_to_rounds in potential_transition_function.items() + if state in destination_states or state is new_initial_round_cls + } + new_final_states = { + state for state in potential_final_states if state in destination_states + } + + # Remove no longer used events + used_events: Set[str] = set() + for event_to_states in new_transition_function.values(): + used_events.update(event_to_states.keys()) + new_events_to_timeout = { + event: timeout + for event, timeout in potential_events_to_timeout.items() + if event in used_events + } + + # Collect keys to persist across periods from all abcis + new_cross_period_persisted_keys: Set[str] = set() + for app in abci_apps: + new_cross_period_persisted_keys.update(app.cross_period_persisted_keys) + + # Return the composed result + class ComposedAbciApp(AbciApp[EventType]): + """Composed abci app class.""" + + initial_round_cls: AppState = new_initial_round_cls + initial_states: Set[AppState] = new_initial_states + transition_function: AbciAppTransitionFunction = new_transition_function + final_states: Set[AppState] = new_final_states + event_to_timeout: EventToTimeout = new_events_to_timeout + cross_period_persisted_keys: FrozenSet[str] = frozenset( + new_cross_period_persisted_keys + ) + db_pre_conditions: Dict[AppState, Set[str]] = new_db_pre_conditions + db_post_conditions: Dict[AppState, Set[str]] = new_db_post_conditions + + return ComposedAbciApp diff --git a/packages/valory/skills/abstract_round_abci/base.py b/packages/valory/skills/abstract_round_abci/base.py new file mode 100644 index 0000000..9718c8e --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/base.py @@ -0,0 +1,3850 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the base classes for the models classes of the skill.""" + +import datetime +import hashlib +import heapq +import itertools +import json +import logging +import re +import sys +import textwrap +import uuid +from abc import ABC, ABCMeta, abstractmethod +from collections import Counter, deque +from copy import copy, deepcopy +from dataclasses import asdict, astuple, dataclass, field, is_dataclass +from enum import Enum +from inspect import isclass +from math import ceil +from typing import ( + Any, + Callable, + Deque, + Dict, + FrozenSet, + Generic, + Iterator, + List, + Mapping, + Optional, + Sequence, + Set, + Tuple, + Type, + TypeVar, + Union, + cast, +) + +from aea.crypto.ledger_apis import LedgerApis +from aea.exceptions import enforce +from aea.skills.base import SkillContext + +from packages.valory.connections.abci.connection import MAX_READ_IN_BYTES +from packages.valory.connections.ledger.connection import ( + PUBLIC_ID as LEDGER_CONNECTION_PUBLIC_ID, +) +from packages.valory.protocols.abci.custom_types import ( + EvidenceType, + Evidences, + Header, + LastCommitInfo, + Validator, +) +from packages.valory.skills.abstract_round_abci.utils import ( + consensus_threshold, + is_json_serializable, +) + + +_logger = logging.getLogger("aea.packages.valory.skills.abstract_round_abci.base") + +OK_CODE = 0 +ERROR_CODE = 1 +LEDGER_API_ADDRESS = str(LEDGER_CONNECTION_PUBLIC_ID) +ROUND_COUNT_DEFAULT = -1 +MIN_HISTORY_DEPTH = 1 +ADDRESS_LENGTH = 42 +MAX_INT_256 = 2**256 - 1 +RESET_COUNT_START = 0 +VALUE_NOT_PROVIDED = object() +# tolerance in seconds for new blocks not having arrived yet +BLOCKS_STALL_TOLERANCE = 60 +SERIOUS_OFFENCE_ENUM_MIN = 1000 +NUMBER_OF_BLOCKS_TRACKED = 10_000 +NUMBER_OF_ROUNDS_TRACKED = 50 + +EventType = TypeVar("EventType") + + +def get_name(prop: Any) -> str: + """Get the name of a property.""" + if not (isinstance(prop, property) and hasattr(prop, "fget")): + raise ValueError(f"{prop} is not a property") + if prop.fget is None: + raise ValueError(f"fget of {prop} is None") # pragma: nocover + return prop.fget.__name__ + + +class ABCIAppException(Exception): + """A parent class for all exceptions related to the ABCIApp.""" + + +class SignatureNotValidError(ABCIAppException): + """Error raised when a signature is invalid.""" + + +class AddBlockError(ABCIAppException): + """Exception raised when a block addition is not valid.""" + + +class ABCIAppInternalError(ABCIAppException): + """Internal error due to a bad implementation of the ABCIApp.""" + + def __init__(self, message: str, *args: Any) -> None: + """Initialize the error object.""" + super().__init__("internal error: " + message, *args) + + +class TransactionTypeNotRecognizedError(ABCIAppException): + """Error raised when a transaction type is not recognized.""" + + +class TransactionNotValidError(ABCIAppException): + """Error raised when a transaction is not valid.""" + + +class LateArrivingTransaction(ABCIAppException): + """Error raised when the transaction belongs to previous round.""" + + +class AbstractRoundInternalError(ABCIAppException): + """Internal error due to a bad implementation of the AbstractRound.""" + + def __init__(self, message: str, *args: Any) -> None: + """Initialize the error object.""" + super().__init__("internal error: " + message, *args) + + +class _MetaPayload(ABCMeta): + """ + Payload metaclass. + + The purpose of this metaclass is to remember the association + between the type of payload and the payload class to build it. + This is necessary to recover the right payload class to instantiate + at decoding time. + """ + + registry: Dict[str, Type["BaseTxPayload"]] = {} + + def __new__(mcs, name: str, bases: Tuple, namespace: Dict, **kwargs: Any) -> Type: # type: ignore + """Create a new class object.""" + new_cls = super().__new__(mcs, name, bases, namespace, **kwargs) + + if new_cls.__module__ == mcs.__module__ and new_cls.__name__ == "BaseTxPayload": + return new_cls + if not issubclass(new_cls, BaseTxPayload): + raise ValueError( # pragma: no cover + f"class {name} must inherit from {BaseTxPayload.__name__}" + ) + new_cls = cast(Type[BaseTxPayload], new_cls) + # remember association from transaction type to payload class + _metaclass_registry_key = f"{new_cls.__module__}.{new_cls.__name__}" # type: ignore + mcs.registry[_metaclass_registry_key] = new_cls + + return new_cls + + +@dataclass(frozen=True) +class BaseTxPayload(metaclass=_MetaPayload): + """This class represents a base class for transaction payload classes.""" + + sender: str + round_count: int = field(default=ROUND_COUNT_DEFAULT, init=False) + id_: str = field(default_factory=lambda: uuid.uuid4().hex, init=False) + + @property + def data(self) -> Dict[str, Any]: + """Data""" + excluded = ["sender", "round_count", "id_"] + return {k: v for k, v in asdict(self).items() if k not in excluded} + + @property + def values(self) -> Tuple[Any, ...]: + """Data""" + excluded = 3 # refers to ["sender", "round_count", "id_"] + return astuple(self)[excluded:] + + @property + def json(self) -> Dict[str, Any]: + """Json""" + data, cls = asdict(self), self.__class__ + data["_metaclass_registry_key"] = f"{cls.__module__}.{cls.__name__}" + return data + + @classmethod + def from_json(cls, obj: Dict) -> "BaseTxPayload": + """Decode the payload.""" + data = copy(obj) + round_count, id_ = data.pop("round_count"), data.pop("id_") + payload_cls = _MetaPayload.registry[data.pop("_metaclass_registry_key")] + payload = payload_cls(**data) # type: ignore + object.__setattr__(payload, "round_count", round_count) + object.__setattr__(payload, "id_", id_) + return payload + + def with_new_id(self) -> "BaseTxPayload": + """Create a new payload with the same content but new id.""" + new = type(self)(sender=self.sender, **self.data) # type: ignore + object.__setattr__(new, "round_count", self.round_count) + return new + + def encode(self) -> bytes: + """Encode""" + encoded_data = json.dumps(self.json, sort_keys=True).encode() + if sys.getsizeof(encoded_data) > MAX_READ_IN_BYTES: + msg = f"{type(self)} must be smaller than {MAX_READ_IN_BYTES} bytes" + raise ValueError(msg) + return encoded_data + + @classmethod + def decode(cls, obj: bytes) -> "BaseTxPayload": + """Decode""" + return cls.from_json(json.loads(obj.decode())) + + +@dataclass(frozen=True) +class Transaction(ABC): + """Class to represent a transaction for the ephemeral chain of a period.""" + + payload: BaseTxPayload + signature: str + + def encode(self) -> bytes: + """Encode the transaction.""" + + data = dict(payload=self.payload.json, signature=self.signature) + encoded_data = json.dumps(data, sort_keys=True).encode() + if sys.getsizeof(encoded_data) > MAX_READ_IN_BYTES: + raise ValueError( + f"Transaction must be smaller than {MAX_READ_IN_BYTES} bytes" + ) + return encoded_data + + @classmethod + def decode(cls, obj: bytes) -> "Transaction": + """Decode the transaction.""" + + data = json.loads(obj.decode()) + signature = data["signature"] + payload = BaseTxPayload.from_json(data["payload"]) + return Transaction(payload, signature) + + def verify(self, ledger_id: str) -> None: + """ + Verify the signature is correct. + + :param ledger_id: the ledger id of the address + :raises: SignatureNotValidError: if the signature is not valid. + """ + payload_bytes = self.payload.encode() + addresses = LedgerApis.recover_message( + identifier=ledger_id, message=payload_bytes, signature=self.signature + ) + if self.payload.sender not in addresses: + raise SignatureNotValidError(f"Signature not valid on transaction: {self}") + + +class Block: # pylint: disable=too-few-public-methods + """Class to represent (a subset of) data of a Tendermint block.""" + + def __init__( + self, + header: Header, + transactions: Sequence[Transaction], + ) -> None: + """Initialize the block.""" + self.header = header + self._transactions: Tuple[Transaction, ...] = tuple(transactions) + + @property + def transactions(self) -> Tuple[Transaction, ...]: + """Get the transactions.""" + return self._transactions + + @property + def timestamp(self) -> datetime.datetime: + """Get the block timestamp.""" + return self.header.timestamp + + +class Blockchain: + """ + Class to represent a (naive) Tendermint blockchain. + + The consistency of the data in the blocks is guaranteed by Tendermint. + """ + + def __init__(self, height_offset: int = 0, is_init: bool = True) -> None: + """Initialize the blockchain.""" + self._blocks: List[Block] = [] + self._height_offset = height_offset + self._is_init = is_init + + @property + def is_init(self) -> bool: + """Returns true if the blockchain is initialized.""" + return self._is_init + + def add_block(self, block: Block) -> None: + """Add a block to the list.""" + expected_height = self.height + 1 + actual_height = block.header.height + if actual_height < self._height_offset: + # if the current block has a lower height than the + # initial height, ignore it + return + + if expected_height != actual_height: + raise AddBlockError( + f"expected height {expected_height}, got {actual_height}" + ) + self._blocks.append(block) + + @property + def height(self) -> int: + """ + Get the height. + + Tendermint's height starts from 1. A return value + equal to 0 means empty blockchain. + + :return: the height. + """ + return self.length + self._height_offset + + @property + def length(self) -> int: + """Get the blockchain length.""" + return len(self._blocks) + + @property + def blocks(self) -> Tuple[Block, ...]: + """Get the blocks.""" + return tuple(self._blocks) + + @property + def last_block( + self, + ) -> Block: + """Returns the last stored block.""" + return self._blocks[-1] + + +class BlockBuilder: + """Helper class to build a block.""" + + _current_header: Optional[Header] = None + _current_transactions: List[Transaction] = [] + + def __init__(self) -> None: + """Initialize the block builder.""" + self.reset() + + def reset(self) -> None: + """Reset the temporary data structures.""" + self._current_header = None + self._current_transactions = [] + + @property + def header(self) -> Header: + """ + Get the block header. + + :return: the block header + """ + if self._current_header is None: + raise ValueError("header not set") + return self._current_header + + @header.setter + def header(self, header: Header) -> None: + """Set the header.""" + if self._current_header is not None: + raise ValueError("header already set") + self._current_header = header + + @property + def transactions(self) -> Tuple[Transaction, ...]: + """Get the sequence of transactions.""" + return tuple(self._current_transactions) + + def add_transaction(self, transaction: Transaction) -> None: + """Add a transaction.""" + self._current_transactions.append(transaction) + + def get_block(self) -> Block: + """Get the block.""" + return Block( + self.header, + self._current_transactions, + ) + + +class AbciAppDB: + """Class to represent all data replicated across agents. + + This class stores all the data in self._data. Every entry on this dict represents an optional "period" within your app execution. + The concept of period is user-defined, so it might be something like a sequence of rounds that together conform a logical cycle of + its execution, or it might have no sense at all (thus its optionality) and therefore only period 0 will be used. + + Every "period" entry stores a dict where every key is a saved parameter and its corresponding value a list containing the history + of the parameter values. For instance, for period 0: + + 0: {"parameter_name": [parameter_history]} + + A complete database could look like this: + + data = { + 0: { + "participants": + [ + {"participant_a", "participant_b", "participant_c", "participant_d"}, + {"participant_a", "participant_b", "participant_c"}, + {"participant_a", "participant_b", "participant_c", "participant_d"}, + ] + }, + "other_parameter": [0, 2, 8] + }, + 1: { + "participants": + [ + {"participant_a", "participant_c", "participant_d"}, + {"participant_a", "participant_b", "participant_c", "participant_d"}, + {"participant_a", "participant_b", "participant_c"}, + {"participant_a", "participant_b", "participant_d"}, + {"participant_a", "participant_b", "participant_c", "participant_d"}, + ], + "other_parameter": [3, 19, 10, 32, 6] + }, + 2: ... + } + + # Adding and removing data from the current period + -------------------------------------------------- + To update the current period entry, just call update() on the class. The new values will be appended to the current list for each updated parameter. + + To clean up old data from the current period entry, call cleanup_current_histories(cleanup_history_depth_current), where cleanup_history_depth_current + is the amount of data that you want to keep after the cleanup. The newest cleanup_history_depth_current values will be kept for each parameter in the DB. + + # Creating and removing old periods + ----------------------------------- + To create a new period entry, call create() on the class. The new values will be stored in a new list for each updated parameter. + + To remove old periods, call cleanup(cleanup_history_depth, [cleanup_history_depth_current]), where cleanup_history_depth is the amount of periods + that you want to keep after the cleanup. The newest cleanup_history_depth periods will be kept. If you also specify cleanup_history_depth_current, + cleanup_current_histories will be also called (see previous point). + + The parameters cleanup_history_depth and cleanup_history_depth_current can also be configured in skill.yaml so they are used automatically + when the cleanup method is called from AbciApp.cleanup(). + + # Memory warning + ----------------------------------- + The database is implemented in such a way to avoid indirect modification of its contents. + It copies all the mutable data structures*, which means that it consumes more memory than expected. + This is necessary because otherwise it would risk chance of modification from the behaviour side, + which is a safety concern. + + The effect of this on the memory usage should not be a big concern, because: + + 1. The synchronized data of the agents are not intended to store large amount of data. + IPFS should be used in such cases, and only the hash should be synchronized in the db. + 2. The data are automatically wiped after a predefined `cleanup_history` depth as described above. + 3. The retrieved data are only meant to be used for a short amount of time, + e.g., to perform a decision on a behaviour, which means that the gc will collect them before they are noticed. + + * the in-built `copy` module is used, which automatically detects if an item is immutable and skips copying it. + For more information take a look at the `_deepcopy_atomic` method and its usage: + https://github.com/python/cpython/blob/3.10/Lib/copy.py#L182-L183 + """ + + DB_DATA_KEY = "db_data" + SLASHING_CONFIG_KEY = "slashing_config" + + # database keys which values are always set for the next period by default + default_cross_period_keys: FrozenSet[str] = frozenset( + { + "all_participants", + "participants", + "consensus_threshold", + "safe_contract_address", + } + ) + + def __init__( + self, + setup_data: Dict[str, List[Any]], + cross_period_persisted_keys: Optional[FrozenSet[str]] = None, + logger: Optional[logging.Logger] = None, + ) -> None: + """Initialize the AbciApp database. + + setup_data must be passed as a Dict[str, List[Any]] (the database internal format). + The staticmethod 'data_to_lists' can be used to convert from Dict[str, Any] to Dict[str, List[Any]] + before instantiating this class. + + :param setup_data: the setup data + :param cross_period_persisted_keys: data keys that will be kept after a new period starts + :param logger: the logger of the abci app + """ + self.logger = logger or _logger + AbciAppDB._check_data(setup_data) + self._setup_data = deepcopy(setup_data) + self._data: Dict[int, Dict[str, List[Any]]] = { + RESET_COUNT_START: self.setup_data # the key represents the reset index + } + self._round_count = ROUND_COUNT_DEFAULT # ensures first round is indexed at 0! + + self._cross_period_persisted_keys = self.default_cross_period_keys.union( + cross_period_persisted_keys or frozenset() + ) + self._cross_period_check() + self.slashing_config: str = "" + + def _cross_period_check(self) -> None: + """Check the cross period keys against the setup data.""" + not_in_cross_period = set(self._setup_data).difference( + self.cross_period_persisted_keys + ) + if not_in_cross_period: + self.logger.warning( + f"The setup data ({self._setup_data.keys()}) contain keys that are not in the " + f"cross period persisted keys ({self.cross_period_persisted_keys}): {not_in_cross_period}" + ) + + @staticmethod + def normalize(value: Any) -> str: + """Attempt to normalize a non-primitive type to insert it into the db.""" + if is_json_serializable(value): + return value + + if isinstance(value, Enum): + return value.value + + if isinstance(value, bytes): + return value.hex() + + if isinstance(value, set): + try: + return json.dumps(list(value)) + except TypeError: + pass + + raise ValueError(f"Cannot normalize {value} to insert it in the db!") + + @property + def setup_data(self) -> Dict[str, Any]: + """ + Get the setup_data without entries which have empty values. + + :return: the setup_data + """ + # do not return data if no value has been set + return {k: v for k, v in deepcopy(self._setup_data).items() if len(v)} + + @staticmethod + def _check_data(data: Any) -> None: + """Check that all fields in setup data were passed as a list, and that the data can be accepted into the db.""" + if ( + not isinstance(data, dict) + or not all((isinstance(k, str) for k in data.keys())) + or not all((isinstance(v, list) for v in data.values())) + ): + raise ValueError( + f"AbciAppDB data must be `Dict[str, List[Any]]`, found `{type(data)}` instead." + ) + + AbciAppDB.validate(data) + + @property + def reset_index(self) -> int: + """Get the current reset index.""" + # should return the last key or 0 if we have no data + return list(self._data)[-1] if self._data else 0 + + @property + def round_count(self) -> int: + """Get the round count.""" + return self._round_count + + @round_count.setter + def round_count(self, round_count: int) -> None: + """Set the round count.""" + self._round_count = round_count + + @property + def cross_period_persisted_keys(self) -> FrozenSet[str]: + """Keys in the database which are persistent across periods.""" + return self._cross_period_persisted_keys + + def get(self, key: str, default: Any = VALUE_NOT_PROVIDED) -> Optional[Any]: + """Given a key, get its last for the current reset index.""" + if key in self._data[self.reset_index]: + return deepcopy(self._data[self.reset_index][key][-1]) + if default != VALUE_NOT_PROVIDED: + return default + raise ValueError( + f"'{key}' field is not set for this period [{self.reset_index}] and no default value was provided." + ) + + def get_strict(self, key: str) -> Any: + """Get a value from the data dictionary and raise if it is None.""" + return self.get(key) + + @staticmethod + def validate(data: Any) -> None: + """Validate if the given data are json serializable and therefore can be accepted into the database. + + :param data: the data to check. + :raises ABCIAppInternalError: If the data are not serializable. + """ + if not is_json_serializable(data): + raise ABCIAppInternalError( + f"`AbciAppDB` data must be json-serializable. Please convert non-serializable data in `{data}`. " + "You may use `AbciAppDB.validate(your_data)` to validate your data for the `AbciAppDB`." + ) + + def update(self, **kwargs: Any) -> None: + """Update the current data.""" + self.validate(kwargs) + + # Append new data to the key history + data = self._data[self.reset_index] + for key, value in deepcopy(kwargs).items(): + data.setdefault(key, []).append(value) + + def create(self, **kwargs: Any) -> None: + """Add a new entry to the data. + + Passes automatically the values of the `cross_period_persisted_keys` to the next period. + + :param kwargs: keyword arguments + """ + for key in self.cross_period_persisted_keys.union(kwargs.keys()): + value = kwargs.get(key, VALUE_NOT_PROVIDED) + if value is VALUE_NOT_PROVIDED: + value = self.get_latest().get(key, VALUE_NOT_PROVIDED) + if value is VALUE_NOT_PROVIDED: + raise ABCIAppInternalError( + f"Cross period persisted key `{key}` was not found in the db but was required for the next period." + ) + if isinstance(value, (set, frozenset)): + value = tuple(sorted(value)) + kwargs[key] = value + + data = self.data_to_lists(kwargs) + self._create_from_keys(**data) + + def _create_from_keys(self, **kwargs: Any) -> None: + """Add a new entry to the data using the provided key-value pairs.""" + AbciAppDB._check_data(kwargs) + self._data[self.reset_index + 1] = deepcopy(kwargs) + + def get_latest_from_reset_index(self, reset_index: int) -> Dict[str, Any]: + """Get the latest key-value pairs from the data dictionary for the specified period.""" + return { + key: values[-1] + for key, values in deepcopy(self._data.get(reset_index, {})).items() + } + + def get_latest(self) -> Dict[str, Any]: + """Get the latest key-value pairs from the data dictionary for the current period.""" + return self.get_latest_from_reset_index(self.reset_index) + + def increment_round_count(self) -> None: + """Increment the round count.""" + self._round_count += 1 + + def __repr__(self) -> str: + """Return a string representation of the data.""" + return f"AbciAppDB({self._data})" + + def cleanup( + self, + cleanup_history_depth: int, + cleanup_history_depth_current: Optional[int] = None, + ) -> None: + """Reset the db, keeping only the latest entries (periods). + + If cleanup_history_depth_current has been also set, also clear oldest historic values in the current entry. + + :param cleanup_history_depth: depth to clean up history + :param cleanup_history_depth_current: whether or not to clean up current entry too. + """ + cleanup_history_depth = max(cleanup_history_depth, MIN_HISTORY_DEPTH) + self._data = { + key: self._data[key] + for key in sorted(self._data.keys())[-cleanup_history_depth:] + } + if cleanup_history_depth_current: + self.cleanup_current_histories(cleanup_history_depth_current) + + def cleanup_current_histories(self, cleanup_history_depth_current: int) -> None: + """Reset the parameter histories for the current entry (period), keeping only the latest values for each parameter.""" + cleanup_history_depth_current = max( + cleanup_history_depth_current, MIN_HISTORY_DEPTH + ) + self._data[self.reset_index] = { + key: history[-cleanup_history_depth_current:] + for key, history in self._data[self.reset_index].items() + } + + def serialize(self) -> str: + """Serialize the data of the database to a string.""" + db = { + self.DB_DATA_KEY: self._data, + self.SLASHING_CONFIG_KEY: self.slashing_config, + } + return json.dumps(db, sort_keys=True) + + @staticmethod + def _as_abci_data(data: Dict) -> Dict[int, Any]: + """Hook to load serialized data as `AbciAppDB` data.""" + return {int(index): content for index, content in data.items()} + + def sync(self, serialized_data: str) -> None: + """Synchronize the data using a serialized object. + + :param serialized_data: the serialized data to use in order to sync the db. + :raises ABCIAppInternalError: if the given data cannot be deserialized. + """ + try: + loaded_data = json.loads(serialized_data) + except json.JSONDecodeError as exc: + raise ABCIAppInternalError( + f"Could not decode data using {serialized_data}: {exc}" + ) from exc + + input_report = f"\nThe following serialized data were given: {serialized_data}" + try: + db_data = loaded_data[self.DB_DATA_KEY] + slashing_config = loaded_data[self.SLASHING_CONFIG_KEY] + except KeyError as exc: + raise ABCIAppInternalError( + "Mandatory keys `db_data`, `slashing_config` are missing from the deserialized data: " + f"{loaded_data}{input_report}" + ) from exc + + try: + db_data = self._as_abci_data(db_data) + except AttributeError as exc: + raise ABCIAppInternalError( + f"Could not decode db data with an invalid format: {db_data}{input_report}" + ) from exc + except ValueError as exc: + raise ABCIAppInternalError( + f"An invalid index was found while trying to sync the db using data: {db_data}{input_report}" + ) from exc + + self._check_data(dict(tuple(db_data.values())[0])) + self._data = db_data + self.slashing_config = slashing_config + + def hash(self) -> bytes: + """Create a hash of the data.""" + # Compute the sha256 hash of the serialized data + sha256 = hashlib.sha256() + data = self.serialize() + sha256.update(data.encode("utf-8")) + hash_ = sha256.digest() + self.logger.debug(f"root hash: {hash_.hex()}; data: {data}") + return hash_ + + @staticmethod + def data_to_lists(data: Dict[str, Any]) -> Dict[str, List[Any]]: + """Convert Dict[str, Any] to Dict[str, List[Any]].""" + return {k: [v] for k, v in data.items()} + + +SerializedCollection = Dict[str, Dict[str, Any]] +DeserializedCollection = Mapping[str, BaseTxPayload] + + +class BaseSynchronizedData: + """ + Class to represent the synchronized data. + + This is the relevant data constructed and replicated by the agents. + """ + + # Keys always set by default + # `round_count` and `period_count` need to be guaranteed to be synchronized too: + # + # * `round_count` is only incremented when scheduling a new round, + # which is by definition always a synchronized action. + # * `period_count` comes from the `reset_index` which is the last key of the `self._data`. + # The `self._data` keys are only updated on create, and cleanup operations, + # which are also meant to be synchronized since they are used at the rounds. + default_db_keys: Set[str] = { + "round_count", + "period_count", + "all_participants", + "nb_participants", + "max_participants", + "consensus_threshold", + "safe_contract_address", + } + + def __init__( + self, + db: AbciAppDB, + ) -> None: + """Initialize the synchronized data.""" + self._db = db + + @property + def db(self) -> AbciAppDB: + """Get DB.""" + return self._db + + @property + def round_count(self) -> int: + """Get the round count.""" + return self.db.round_count + + @property + def period_count(self) -> int: + """Get the period count. + + Periods are executions between calls to AbciAppDB.create(), so as soon as it is called, + a new period begins. It is useful to have a logical subdivision of the FSM execution. + For example, if AbciAppDB.create() is called during reset, then a period will be the + execution between resets. + + :return: the period count + """ + return self.db.reset_index + + @property + def participants(self) -> FrozenSet[str]: + """Get the currently active participants.""" + participants = frozenset(self.db.get_strict("participants")) + if len(participants) == 0: + raise ValueError("List participants cannot be empty.") + return cast(FrozenSet[str], participants) + + @property + def all_participants(self) -> FrozenSet[str]: + """Get all registered participants.""" + all_participants = frozenset(self.db.get_strict("all_participants")) + if len(all_participants) == 0: + raise ValueError("List participants cannot be empty.") + return cast(FrozenSet[str], all_participants) + + @property + def max_participants(self) -> int: + """Get the number of all the participants.""" + return len(self.all_participants) + + @property + def consensus_threshold(self) -> int: + """Get the consensus threshold.""" + threshold = self.db.get_strict("consensus_threshold") + min_threshold = consensus_threshold(self.max_participants) + + if threshold is None: + return min_threshold + + threshold = int(threshold) + max_threshold = len(self.all_participants) + + if min_threshold <= threshold <= max_threshold: + return threshold + + expected_range = ( + f"can only be {min_threshold}" + if min_threshold == max_threshold + else f"not in [{min_threshold}, {max_threshold}]" + ) + raise ValueError(f"Consensus threshold {threshold} {expected_range}.") + + @property + def sorted_participants(self) -> Sequence[str]: + """ + Get the sorted participants' addresses. + + The addresses are sorted according to their hexadecimal value; + this is the reason we use key=str.lower as comparator. + + This property is useful when interacting with the Safe contract. + + :return: the sorted participants' addresses + """ + return sorted(self.participants, key=str.lower) + + @property + def nb_participants(self) -> int: + """Get the number of participants.""" + participants = cast(List, self.db.get("participants", [])) + return len(participants) + + @property + def slashing_config(self) -> str: + """Get the slashing configuration.""" + return self.db.slashing_config + + @slashing_config.setter + def slashing_config(self, config: str) -> None: + """Set the slashing configuration.""" + self.db.slashing_config = config + + def update( + self, + synchronized_data_class: Optional[Type] = None, + **kwargs: Any, + ) -> "BaseSynchronizedData": + """Copy and update the current data.""" + self.db.update(**kwargs) + + class_ = ( + type(self) if synchronized_data_class is None else synchronized_data_class + ) + return class_(db=self.db) + + def create( + self, + synchronized_data_class: Optional[Type] = None, + ) -> "BaseSynchronizedData": + """Copy and update with new data. Set values are stored as sorted tuples to the db for determinism.""" + self.db.create() + class_ = ( + type(self) if synchronized_data_class is None else synchronized_data_class + ) + return class_(db=self.db) + + def __repr__(self) -> str: + """Return a string representation of the data.""" + return f"{self.__class__.__name__}(db={self._db})" + + @property + def keeper_randomness(self) -> float: + """Get the keeper's random number [0-1].""" + return ( + int(self.most_voted_randomness, base=16) / MAX_INT_256 + ) # DRAND uses sha256 values + + @property + def most_voted_randomness(self) -> str: + """Get the most_voted_randomness.""" + return cast(str, self.db.get_strict("most_voted_randomness")) + + @property + def most_voted_keeper_address(self) -> str: + """Get the most_voted_keeper_address.""" + return cast(str, self.db.get_strict("most_voted_keeper_address")) + + @property + def is_keeper_set(self) -> bool: + """Check whether keeper is set.""" + return self.db.get("most_voted_keeper_address", None) is not None + + @property + def blacklisted_keepers(self) -> Set[str]: + """Get the current cycle's blacklisted keepers who cannot submit a transaction.""" + raw = cast(str, self.db.get("blacklisted_keepers", "")) + return set(textwrap.wrap(raw, ADDRESS_LENGTH)) + + @property + def participant_to_selection(self) -> DeserializedCollection: + """Check whether keeper is set.""" + serialized = self.db.get_strict("participant_to_selection") + deserialized = CollectionRound.deserialize_collection(serialized) + return cast(DeserializedCollection, deserialized) + + @property + def participant_to_randomness(self) -> DeserializedCollection: + """Check whether keeper is set.""" + serialized = self.db.get_strict("participant_to_randomness") + deserialized = CollectionRound.deserialize_collection(serialized) + return cast(DeserializedCollection, deserialized) + + @property + def participant_to_votes(self) -> DeserializedCollection: + """Check whether keeper is set.""" + serialized = self.db.get_strict("participant_to_votes") + deserialized = CollectionRound.deserialize_collection(serialized) + return cast(DeserializedCollection, deserialized) + + @property + def safe_contract_address(self) -> str: + """Get the safe contract address.""" + return cast(str, self.db.get_strict("safe_contract_address")) + + +class _MetaAbstractRound(ABCMeta): + """A metaclass that validates AbstractRound's attributes.""" + + def __new__(mcs, name: str, bases: Tuple, namespace: Dict, **kwargs: Any) -> Type: # type: ignore + """Initialize the class.""" + new_cls = super().__new__(mcs, name, bases, namespace, **kwargs) + + if ABC in bases: + # abstract class, return + return new_cls + if not issubclass(new_cls, AbstractRound): + # the check only applies to AbstractRound subclasses + return new_cls + + mcs._check_consistency(cast(Type[AbstractRound], new_cls)) + return new_cls + + @classmethod + def _check_consistency(mcs, abstract_round_cls: Type["AbstractRound"]) -> None: + """Check consistency of class attributes.""" + mcs._check_required_class_attributes(abstract_round_cls) + + @classmethod + def _check_required_class_attributes( + mcs, abstract_round_cls: Type["AbstractRound"] + ) -> None: + """Check that required class attributes are set.""" + if not hasattr(abstract_round_cls, "synchronized_data_class"): + raise AbstractRoundInternalError( + f"'synchronized_data_class' not set on {abstract_round_cls}" + ) + if not hasattr(abstract_round_cls, "payload_class"): + raise AbstractRoundInternalError( + f"'payload_class' not set on {abstract_round_cls}" + ) + + +class AbstractRound(Generic[EventType], ABC, metaclass=_MetaAbstractRound): + """ + This class represents an abstract round. + + A round is a state of the FSM App execution. It usually involves + interactions between participants in the FSM App, + although this is not enforced at this level of abstraction. + + Concrete classes must set: + - synchronized_data_class: the data class associated with this round; + - payload_class: the payload type that is allowed for this round; + + Optionally, round_id can be defined, although it is recommended to use the autogenerated id. + """ + + __pattern = re.compile(r"(? None: + """Initialize the round.""" + self._synchronized_data = synchronized_data + self.block_confirmations = 0 + self._previous_round_payload_class = previous_round_payload_class + self.context = context + + @classmethod + def auto_round_id(cls) -> str: + """ + Get round id automatically. + + This method returns the auto generated id from the class name if the + class variable behaviour_id is not set on the child class. + Otherwise, it returns the class variable behaviour_id. + """ + return ( + cls.round_id + if isinstance(cls.round_id, str) + else cls.__pattern.sub("_", cls.__name__).lower() + ) + + @property # type: ignore + def round_id(self) -> str: + """Get round id.""" + return self.auto_round_id() + + @property + def synchronized_data(self) -> BaseSynchronizedData: + """Get the synchronized data.""" + return self._synchronized_data + + def check_transaction(self, transaction: Transaction) -> None: + """ + Check transaction against the current state. + + :param transaction: the transaction + """ + self.check_payload_type(transaction) + self.check_payload(transaction.payload) + + def process_transaction(self, transaction: Transaction) -> None: + """ + Process a transaction. + + By convention, the payload handler should be a method + of the class that is named '{payload_name}'. + + :param transaction: the transaction. + """ + self.check_payload_type(transaction) + self.process_payload(transaction.payload) + + @abstractmethod + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: + """ + Process the end of the block. + + The role of this method is check whether the round + is considered ended. + + If the round is ended, the return value is + - the final result of the round. + - the event that triggers a transition. If None, the period + in which the round was executed is considered ended. + + This is done after each block because we consider the consensus engine's + block, and not the transaction, as the smallest unit + on which the consensus is reached; in other words, + each read operation on the state should be done + only after each block, and not after each transaction. + """ + + def check_payload_type(self, transaction: Transaction) -> None: + """ + Check the transaction is of the allowed transaction type. + + :param transaction: the transaction + :raises: TransactionTypeNotRecognizedError if the transaction can be + applied to the current state. + """ + if self.payload_class is None: + raise TransactionTypeNotRecognizedError( + "current round does not allow transactions" + ) + + payload_class = type(transaction.payload) + + if payload_class is self._previous_round_payload_class: + raise LateArrivingTransaction( + f"request '{transaction.payload}' is from previous round; skipping" + ) + + if payload_class is not self.payload_class: + raise TransactionTypeNotRecognizedError( + f"request '{payload_class}' not recognized; only {self.payload_class} is supported" + ) + + def check_majority_possible_with_new_voter( + self, + votes_by_participant: Dict[str, BaseTxPayload], + new_voter: str, + new_vote: BaseTxPayload, + nb_participants: int, + exception_cls: Type[ABCIAppException] = ABCIAppException, + ) -> None: + """ + Check that a Byzantine majority is achievable, once a new vote is added. + + :param votes_by_participant: a mapping from a participant to its vote, + before the new vote is added + :param new_voter: the new voter + :param new_vote: the new vote + :param nb_participants: the total number of participants + :param exception_cls: the class of the exception to raise in case the + check fails. + :raises: exception_cls: in case the check does not pass. + """ + # check preconditions + enforce( + new_voter not in votes_by_participant, + "voter has already voted", + ABCIAppInternalError, + ) + enforce( + len(votes_by_participant) <= nb_participants - 1, + "nb_participants not consistent with votes_by_participants", + ABCIAppInternalError, + ) + + # copy the input dictionary to avoid side effects + votes_by_participant = copy(votes_by_participant) + + # add the new vote + votes_by_participant[new_voter] = new_vote + + self.check_majority_possible( + votes_by_participant, nb_participants, exception_cls=exception_cls + ) + + def check_majority_possible( + self, + votes_by_participant: Dict[str, BaseTxPayload], + nb_participants: int, + exception_cls: Type[ABCIAppException] = ABCIAppException, + ) -> None: + """ + Check that a Byzantine majority is still achievable. + + The idea is that, even if all the votes have not been delivered yet, + it can be deduced whether a quorum cannot be reached due to + divergent preferences among the voters and due to a too small + number of other participants whose vote has not been delivered yet. + + The check fails iff: + + nb_remaining_votes + largest_nb_votes < quorum + + That is, if the number of remaining votes is not enough to make + the most voted item so far to exceed the quorum. + + Preconditions on the input: + - the size of votes_by_participant should not be greater than + "nb_participants - 1" voters + - new voter must not be in the current votes_by_participant + + :param votes_by_participant: a mapping from a participant to its vote + :param nb_participants: the total number of participants + :param exception_cls: the class of the exception to raise in case the + check fails. + :raises exception_cls: in case the check does not pass. + """ + enforce( + nb_participants > 0 and len(votes_by_participant) <= nb_participants, + "nb_participants not consistent with votes_by_participants", + ABCIAppInternalError, + ) + if len(votes_by_participant) == 0: + return + + votes = votes_by_participant.values() + vote_count = Counter(tuple(sorted(v.data.items())) for v in votes) + largest_nb_votes = max(vote_count.values()) + nb_votes_received = sum(vote_count.values()) + nb_remaining_votes = nb_participants - nb_votes_received + + if ( + nb_remaining_votes + largest_nb_votes + < self.synchronized_data.consensus_threshold + ): + raise exception_cls( + f"cannot reach quorum={self.synchronized_data.consensus_threshold}, " + f"number of remaining votes={nb_remaining_votes}, number of most voted item's votes={largest_nb_votes}" + ) + + def is_majority_possible( + self, votes_by_participant: Dict[str, BaseTxPayload], nb_participants: int + ) -> bool: + """ + Return true if a Byzantine majority is achievable, false otherwise. + + :param votes_by_participant: a mapping from a participant to its vote + :param nb_participants: the total number of participants + :return: True if the majority is still possible, false otherwise. + """ + try: + self.check_majority_possible(votes_by_participant, nb_participants) + except ABCIAppException: + return False + return True + + @abstractmethod + def check_payload(self, payload: BaseTxPayload) -> None: + """Check payload.""" + + @abstractmethod + def process_payload(self, payload: BaseTxPayload) -> None: + """Process payload.""" + + +class DegenerateRound(AbstractRound, ABC): + """ + This class represents the finished round during operation. + + It is a sink round. + """ + + payload_class = None + synchronized_data_class = BaseSynchronizedData + + def check_payload(self, payload: BaseTxPayload) -> None: + """Check payload.""" + raise NotImplementedError( # pragma: nocover + "DegenerateRound should not be used in operation." + ) + + def process_payload(self, payload: BaseTxPayload) -> None: + """Process payload.""" + raise NotImplementedError( # pragma: nocover + "DegenerateRound should not be used in operation." + ) + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: + """End block.""" + raise NotImplementedError( # pragma: nocover + "DegenerateRound should not be used in operation." + ) + + +class CollectionRound(AbstractRound, ABC): + """ + CollectionRound. + + This class represents abstract logic for collection based rounds where + the round object needs to collect data from different agents. The data + might for example be from a voting round or estimation round. + + `_allow_rejoin_payloads` is used to allow agents not currently active to + deliver a payload. + """ + + _allow_rejoin_payloads: bool = False + + def __init__(self, *args: Any, **kwargs: Any): + """Initialize the collection round.""" + super().__init__(*args, **kwargs) + self.collection: Dict[str, BaseTxPayload] = {} + + @staticmethod + def serialize_collection( + collection: DeserializedCollection, + ) -> SerializedCollection: + """Deserialize a serialized collection.""" + return {address: payload.json for address, payload in collection.items()} + + @staticmethod + def deserialize_collection( + serialized: SerializedCollection, + ) -> DeserializedCollection: + """Deserialize a serialized collection.""" + return { + address: BaseTxPayload.from_json(payload_json) + for address, payload_json in serialized.items() + } + + @property + def serialized_collection(self) -> SerializedCollection: + """A collection with the addresses mapped to serialized payloads.""" + return self.serialize_collection(self.collection) + + @property + def accepting_payloads_from(self) -> FrozenSet[str]: + """Accepting from the active set, or also from (re)joiners""" + if self._allow_rejoin_payloads: + return self.synchronized_data.all_participants + return self.synchronized_data.participants + + @property + def payloads(self) -> List[BaseTxPayload]: + """Get all agent payloads""" + return list(self.collection.values()) + + @property + def payload_values_count(self) -> Counter: + """Get count of payload values.""" + return Counter(map(lambda p: p.values, self.payloads)) + + def process_payload(self, payload: BaseTxPayload) -> None: + """Process payload.""" + if payload.round_count != self.synchronized_data.round_count: + raise ABCIAppInternalError( + f"Expected round count {self.synchronized_data.round_count} and got {payload.round_count}." + ) + + sender = payload.sender + if sender not in self.accepting_payloads_from: + raise ABCIAppInternalError( + f"{sender} not in list of participants: {sorted(self.accepting_payloads_from)}" + ) + + if sender in self.collection: + raise ABCIAppInternalError( + f"sender {sender} has already sent value for round: {self.round_id}" + ) + + self.collection[sender] = payload + + def check_payload(self, payload: BaseTxPayload) -> None: + """Check Payload""" + + # NOTE: the TransactionNotValidError is intercepted in ABCIRoundHandler.deliver_tx + # which means it will be logged instead of raised + if payload.round_count != self.synchronized_data.round_count: + raise TransactionNotValidError( + f"Expected round count {self.synchronized_data.round_count} and got {payload.round_count}." + ) + + sender_in_participant_set = payload.sender in self.accepting_payloads_from + if not sender_in_participant_set: + raise TransactionNotValidError( + f"{payload.sender} not in list of participants: {sorted(self.accepting_payloads_from)}" + ) + + if payload.sender in self.collection: + raise TransactionNotValidError( + f"sender {payload.sender} has already sent value for round: {self.round_id}" + ) + + +class _CollectUntilAllRound(CollectionRound, ABC): + """ + _CollectUntilAllRound + + This class represents abstract logic for when rounds need to collect payloads from all agents. + + This round should only be used when non-BFT behaviour is acceptable. + """ + + def check_payload(self, payload: BaseTxPayload) -> None: + """Check Payload""" + if payload.round_count != self.synchronized_data.round_count: + raise TransactionNotValidError( + f"Expected round count {self.synchronized_data.round_count} and got {payload.round_count}." + ) + + if payload.sender in self.collection: + raise TransactionNotValidError( + f"sender {payload.sender} has already sent value for round: {self.round_id}" + ) + + def process_payload(self, payload: BaseTxPayload) -> None: + """Process payload.""" + try: + self.check_payload(payload) + except TransactionNotValidError as e: + raise ABCIAppInternalError(e.args[0]) from e + + self.collection[payload.sender] = payload + + @property + def collection_threshold_reached( + self, + ) -> bool: + """Check that the collection threshold has been reached.""" + return len(self.collection) >= self.synchronized_data.max_participants + + +class CollectDifferentUntilAllRound(_CollectUntilAllRound, ABC): + """ + CollectDifferentUntilAllRound + + This class represents logic for rounds where a round needs to collect + different payloads from each agent. + + This round should only be used for registration of new agents when there is synchronization of the db. + """ + + def check_payload(self, payload: BaseTxPayload) -> None: + """Check Payload""" + new = payload.values + existing = [payload_.values for payload_ in self.collection.values()] + + if payload.sender not in self.collection and new in existing: + raise TransactionNotValidError( + f"`CollectDifferentUntilAllRound` encountered a value '{new}' that already exists. " + f"All values: {existing}" + ) + + super().check_payload(payload) + + +class CollectSameUntilAllRound(_CollectUntilAllRound, ABC): + """ + This class represents logic for when a round needs to collect the same payload from all the agents. + + This round should only be used for registration of new agents when there is no synchronization of the db. + """ + + def check_payload(self, payload: BaseTxPayload) -> None: + """Check Payload""" + new = payload.values + existing_ = [payload_.values for payload_ in self.collection.values()] + + if ( + payload.sender not in self.collection + and len(self.collection) + and new not in existing_ + ): + raise TransactionNotValidError( + f"`CollectSameUntilAllRound` encountered a value '{new}' " + f"which is not the same as the already existing one: '{existing_[0]}'" + ) + + super().check_payload(payload) + + @property + def common_payload( + self, + ) -> Any: + """Get the common payload among the agents.""" + return self.common_payload_values[0] + + @property + def common_payload_values( + self, + ) -> Tuple[Any, ...]: + """Get the common payload among the agents.""" + most_common_payload_values, max_votes = self.payload_values_count.most_common( + 1 + )[0] + if max_votes < self.synchronized_data.max_participants: + raise ABCIAppInternalError( + f"{max_votes} votes are not enough for `CollectSameUntilAllRound`. Expected: " + f"`n_votes = max_participants = {self.synchronized_data.max_participants}`" + ) + return most_common_payload_values + + +class CollectSameUntilThresholdRound(CollectionRound, ABC): + """ + CollectSameUntilThresholdRound + + This class represents logic for rounds where a round needs to collect + same payload from k of n agents. + + `done_event` is emitted when a) the collection threshold (k of n) is reached, + and b) the most voted payload has non-empty attributes. In this case all + payloads are saved under `collection_key` and the most voted payload attributes + are saved under `selection_key`. + + `none_event` is emitted when a) the collection threshold (k of n) is reached, + and b) the most voted payload has only empty attributes. + + `no_majority_event` is emitted when it is impossible to reach a k of n majority. + """ + + done_event: Any + no_majority_event: Any + none_event: Any + collection_key: str + selection_key: Union[str, Tuple[str, ...]] + + @property + def threshold_reached( + self, + ) -> bool: + """Check if the threshold has been reached.""" + counts = self.payload_values_count.values() + return any( + count >= self.synchronized_data.consensus_threshold for count in counts + ) + + @property + def most_voted_payload( + self, + ) -> Any: + """ + Get the most voted payload value. + + Kept for backward compatibility. + """ + return self.most_voted_payload_values[0] + + @property + def most_voted_payload_values( + self, + ) -> Tuple[Any, ...]: + """Get the most voted payload values.""" + most_voted_payload_values, max_votes = self.payload_values_count.most_common()[ + 0 + ] + if max_votes < self.synchronized_data.consensus_threshold: + raise ABCIAppInternalError("not enough votes") + return most_voted_payload_values + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: + """Process the end of the block.""" + if self.threshold_reached and any( + [val is not None for val in self.most_voted_payload_values] + ): + if isinstance(self.selection_key, tuple): + data = dict(zip(self.selection_key, self.most_voted_payload_values)) + data[self.collection_key] = self.serialized_collection + else: + data = { + self.collection_key: self.serialized_collection, + self.selection_key: self.most_voted_payload, + } + synchronized_data = self.synchronized_data.update( + synchronized_data_class=self.synchronized_data_class, + **data, + ) + return synchronized_data, self.done_event + if self.threshold_reached and not any( + [val is not None for val in self.most_voted_payload_values] + ): + return self.synchronized_data, self.none_event + if not self.is_majority_possible( + self.collection, self.synchronized_data.nb_participants + ): + return self.synchronized_data, self.no_majority_event + return None + + +class OnlyKeeperSendsRound(AbstractRound, ABC): + """ + OnlyKeeperSendsRound + + This class represents logic for rounds where only one agent sends a + payload. + + `done_event` is emitted when a) the keeper payload has been received and b) + the keeper payload has non-empty attributes. In this case all attributes are saved + under `payload_key`. + + `fail_event` is emitted when a) the keeper payload has been received and b) + the keeper payload has only empty attributes + """ + + keeper_payload: Optional[BaseTxPayload] = None + done_event: Any + fail_event: Any + payload_key: Union[str, Tuple[str, ...]] + + def process_payload(self, payload: BaseTxPayload) -> None: + """Handle a deploy safe payload.""" + if payload.round_count != self.synchronized_data.round_count: + raise ABCIAppInternalError( + f"Expected round count {self.synchronized_data.round_count} and got {payload.round_count}." + ) + + sender = payload.sender + + if sender not in self.synchronized_data.participants: + raise ABCIAppInternalError( + f"{sender} not in list of participants: {sorted(self.synchronized_data.participants)}" + ) + + if sender != self.synchronized_data.most_voted_keeper_address: + raise ABCIAppInternalError(f"{sender} not elected as keeper.") + + if self.keeper_payload is not None: + raise ABCIAppInternalError("keeper already set the payload.") + + self.keeper_payload = payload + + def check_payload(self, payload: BaseTxPayload) -> None: + """Check a deploy safe payload can be applied to the current state.""" + if payload.round_count != self.synchronized_data.round_count: + raise TransactionNotValidError( + f"Expected round count {self.synchronized_data.round_count} and got {payload.round_count}." + ) + + sender = payload.sender + sender_in_participant_set = sender in self.synchronized_data.participants + if not sender_in_participant_set: + raise TransactionNotValidError( + f"{sender} not in list of participants: {sorted(self.synchronized_data.participants)}" + ) + + sender_is_elected_sender = ( + sender == self.synchronized_data.most_voted_keeper_address + ) + if not sender_is_elected_sender: + raise TransactionNotValidError(f"{sender} not elected as keeper.") + + if self.keeper_payload is not None: + raise TransactionNotValidError("keeper payload value already set.") + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: + """Process the end of the block.""" + if self.keeper_payload is not None and any( + [val is not None for val in self.keeper_payload.values] + ): + if isinstance(self.payload_key, tuple): + data = dict(zip(self.payload_key, self.keeper_payload.values)) + else: + data = { + self.payload_key: self.keeper_payload.values[0], + } + synchronized_data = self.synchronized_data.update( + synchronized_data_class=self.synchronized_data_class, + **data, + ) + return synchronized_data, self.done_event + if self.keeper_payload is not None and not any( + [val is not None for val in self.keeper_payload.values] + ): + return self.synchronized_data, self.fail_event + return None + + +class VotingRound(CollectionRound, ABC): + """ + VotingRound + + This class represents logic for rounds where a round needs votes from + agents. Votes are in the form of `True` (positive), `False` (negative) + and `None` (abstain). The round ends when k of n agents make the same vote. + + `done_event` is emitted when a) the collection threshold (k of n) is reached + with k positive votes. In this case all payloads are saved under `collection_key`. + + `negative_event` is emitted when a) the collection threshold (k of n) is reached + with k negative votes. + + `none_event` is emitted when a) the collection threshold (k of n) is reached + with k abstain votes. + + `no_majority_event` is emitted when it is impossible to reach a k of n majority for + either of the options. + """ + + done_event: Any + negative_event: Any + none_event: Any + no_majority_event: Any + collection_key: str + + @property + def vote_count(self) -> Counter: + """Get agent payload vote count""" + + def parse_payload(payload: Any) -> Optional[bool]: + if not hasattr(payload, "vote"): + raise ValueError(f"payload {payload} has no attribute `vote`") + return payload.vote + + return Counter(parse_payload(payload) for payload in self.collection.values()) + + @property + def positive_vote_threshold_reached(self) -> bool: + """Check that the vote threshold has been reached.""" + return self.vote_count[True] >= self.synchronized_data.consensus_threshold + + @property + def negative_vote_threshold_reached(self) -> bool: + """Check that the vote threshold has been reached.""" + return self.vote_count[False] >= self.synchronized_data.consensus_threshold + + @property + def none_vote_threshold_reached(self) -> bool: + """Check that the vote threshold has been reached.""" + return self.vote_count[None] >= self.synchronized_data.consensus_threshold + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: + """Process the end of the block.""" + if self.positive_vote_threshold_reached: + synchronized_data = self.synchronized_data.update( + synchronized_data_class=self.synchronized_data_class, + **{self.collection_key: self.serialized_collection}, + ) + return synchronized_data, self.done_event + if self.negative_vote_threshold_reached: + return self.synchronized_data, self.negative_event + if self.none_vote_threshold_reached: + return self.synchronized_data, self.none_event + if not self.is_majority_possible( + self.collection, self.synchronized_data.nb_participants + ): + return self.synchronized_data, self.no_majority_event + return None + + +class CollectDifferentUntilThresholdRound(CollectionRound, ABC): + """ + CollectDifferentUntilThresholdRound + + This class represents logic for rounds where a round needs to collect + different payloads from k of n agents. + + `done_event` is emitted when a) the required block confirmations + have been met, and b) the collection threshold (k of n) is reached. In + this case all payloads are saved under `collection_key`. + + Extended `required_block_confirmations` to allow for arrival of more + payloads. + """ + + done_event: Any + collection_key: str + required_block_confirmations: int = 0 + + @property + def collection_threshold_reached( + self, + ) -> bool: + """Check if the threshold has been reached.""" + return len(self.collection) >= self.synchronized_data.consensus_threshold + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: + """Process the end of the block.""" + if self.collection_threshold_reached: + self.block_confirmations += 1 + if ( + self.collection_threshold_reached + and self.block_confirmations > self.required_block_confirmations + ): + synchronized_data = self.synchronized_data.update( + synchronized_data_class=self.synchronized_data_class, + **{ + self.collection_key: self.serialized_collection, + }, + ) + return synchronized_data, self.done_event + + return None + + +class CollectNonEmptyUntilThresholdRound(CollectDifferentUntilThresholdRound, ABC): + """ + CollectNonEmptyUntilThresholdRound + + This class represents logic for rounds where a round needs to collect + optionally different payloads from k of n agents, where we only keep the non-empty attributes. + + `done_event` is emitted when a) the required block confirmations + have been met, b) the collection threshold (k of n) is reached, and + c) some non-empty attribute values have been collected. In this case + all payloads are saved under `collection_key`. Under `selection_key` + the non-empty attribute values are stored. + + `none_event` is emitted when a) the required block confirmations + have been met, b) the collection threshold (k of n) is reached, and + c) no non-empty attribute values have been collected. + + Attention: A `none_event` might be triggered even though some of the + remaining n-k agents might send non-empty attributes! Extended + `required_block_confirmations` can alleviate this somewhat. + """ + + none_event: Any + selection_key: Union[str, Tuple[str, ...]] + + def _get_non_empty_values(self) -> Dict[str, Tuple[Any, ...]]: + """Get the non-empty values from the payload, for all attributes.""" + non_empty_values: Dict[str, List[List[Any]]] = {} + + for sender, payload in self.collection.items(): + if sender not in non_empty_values: + non_empty_values[sender] = [ + value for value in payload.values if value is not None + ] + if len(non_empty_values[sender]) == 0: + del non_empty_values[sender] + continue + non_empty_values_ = { + sender: tuple(li) for sender, li in non_empty_values.items() + } + return non_empty_values_ + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: + """Process the end of the block.""" + if self.collection_threshold_reached: + self.block_confirmations += 1 + if ( + self.collection_threshold_reached + and self.block_confirmations > self.required_block_confirmations + ): + non_empty_values = self._get_non_empty_values() + + if isinstance(self.selection_key, tuple): + data: Dict[str, Any] = { + sender: dict(zip(self.selection_key, values)) + for sender, values in non_empty_values.items() + } + else: + data = { + self.selection_key: { + sender: values[0] for sender, values in non_empty_values.items() + }, + } + data[self.collection_key] = self.serialized_collection + + synchronized_data = self.synchronized_data.update( + synchronized_data_class=self.synchronized_data_class, + **data, + ) + + if all([len(tu) == 0 for tu in non_empty_values]): + return self.synchronized_data, self.none_event + return synchronized_data, self.done_event + return None + + +AppState = Type[AbstractRound] +AbciAppTransitionFunction = Dict[AppState, Dict[EventType, AppState]] +EventToTimeout = Dict[EventType, float] + + +@dataclass(order=True) +class TimeoutEvent(Generic[EventType]): + """Timeout event.""" + + deadline: datetime.datetime + entry_count: int + event: EventType = field(compare=False) + cancelled: bool = field(default=False, compare=False) + + +class Timeouts(Generic[EventType]): + """Class to keep track of pending timeouts.""" + + def __init__(self) -> None: + """Initialize.""" + # The entry count serves as a tie-breaker so that two tasks with + # the same priority are returned in the order they were added + self._counter = itertools.count() + + # The timeout priority queue keeps the earliest deadline at the top. + self._heap: List[TimeoutEvent[EventType]] = [] + + # Mapping from entry id to task + self._entry_finder: Dict[int, TimeoutEvent[EventType]] = {} + + @property + def size(self) -> int: + """Get the size of the timeout queue.""" + return len(self._heap) + + def add_timeout(self, deadline: datetime.datetime, event: EventType) -> int: + """Add a timeout.""" + entry_count = next(self._counter) + timeout_event = TimeoutEvent[EventType](deadline, entry_count, event) + heapq.heappush(self._heap, timeout_event) + self._entry_finder[entry_count] = timeout_event + return entry_count + + def cancel_timeout(self, entry_count: int) -> None: + """ + Remove a timeout. + + :param entry_count: the entry id to remove. + :raises: KeyError: if the entry count is not found. + """ + if entry_count in self._entry_finder: + self._entry_finder[entry_count].cancelled = True + + def pop_earliest_cancelled_timeouts(self) -> None: + """Pop earliest cancelled timeouts.""" + if self.size == 0: + return + entry = self._heap[0] # heap peak + while entry.cancelled: + self.pop_timeout() + if self.size == 0: + break + entry = self._heap[0] + + def get_earliest_timeout(self) -> Tuple[datetime.datetime, Any]: + """Get the earliest timeout-event pair.""" + entry = self._heap[0] + return entry.deadline, entry.event + + def pop_timeout(self) -> Tuple[datetime.datetime, Any]: + """Remove and return the earliest timeout-event pair.""" + entry = heapq.heappop(self._heap) + del self._entry_finder[entry.entry_count] + return entry.deadline, entry.event + + +class _MetaAbciApp(ABCMeta): + """A metaclass that validates AbciApp's attributes.""" + + bg_round_added: bool = False + + def __new__(mcs, name: str, bases: Tuple, namespace: Dict, **kwargs: Any) -> Type: # type: ignore + """Initialize the class.""" + new_cls = super().__new__(mcs, name, bases, namespace, **kwargs) + + if ABC in bases: + # abstract class, return + return new_cls + if not issubclass(new_cls, AbciApp): + # the check only applies to AbciApp subclasses + return new_cls + + if not mcs.bg_round_added: + mcs._add_pending_offences_bg_round(new_cls) + mcs.bg_round_added = True + + mcs._check_consistency(cast(Type[AbciApp], new_cls)) + + return new_cls + + @classmethod + def _check_consistency(mcs, abci_app_cls: Type["AbciApp"]) -> None: + """Check consistency of class attributes.""" + mcs._check_required_class_attributes(abci_app_cls) + mcs._check_initial_states_and_final_states(abci_app_cls) + mcs._check_consistency_outgoing_transitions_from_non_final_states(abci_app_cls) + mcs._check_db_constraints_consistency(abci_app_cls) + + @classmethod + def _check_required_class_attributes(mcs, abci_app_cls: Type["AbciApp"]) -> None: + """Check that required class attributes are set.""" + if not hasattr(abci_app_cls, "initial_round_cls"): + raise ABCIAppInternalError("'initial_round_cls' field not set") + if not hasattr(abci_app_cls, "transition_function"): + raise ABCIAppInternalError("'transition_function' field not set") + + @classmethod + def _check_initial_states_and_final_states( + mcs, + abci_app_cls: Type["AbciApp"], + ) -> None: + """ + Check that initial states and final states are consistent. + + I.e.: + - check that all the initial states are in the set of states specified + by the transition function. + - check that the initial state has outgoing transitions + - check that the initial state does not trigger timeout events. This is + because we need at least one block/timestamp to start timeouts. + - check that initial states are not final states. + - check that the set of final states is a proper subset of the set of + states. + - check that a final state does not have outgoing transitions. + + :param abci_app_cls: the AbciApp class + """ + initial_round_cls = abci_app_cls.initial_round_cls + initial_states = abci_app_cls.initial_states + transition_function = abci_app_cls.transition_function + final_states = abci_app_cls.final_states + states = abci_app_cls.get_all_rounds() + + enforce( + initial_states == set() or initial_round_cls in initial_states, + f"initial round class {initial_round_cls} is not in the set of " + f"initial states: {initial_states}", + ) + enforce( + initial_round_cls in states + and all(initial_state in states for initial_state in initial_states), + "initial states must be in the set of states", + ) + + true_initial_states = ( + initial_states if initial_states != set() else {initial_round_cls} + ) + enforce( + all( + initial_state not in final_states + for initial_state in true_initial_states + ), + "initial states cannot be final states", + ) + + unknown_final_states = set.difference(final_states, states) + enforce( + len(unknown_final_states) == 0, + f"the following final states are not in the set of states:" + f" {unknown_final_states}", + ) + + enforce( + all( + len(transition_function[final_state]) == 0 + for final_state in final_states + ), + "final states cannot have outgoing transitions", + ) + + enforce( + all( + issubclass(final_state, DegenerateRound) for final_state in final_states + ), + "final round classes must be subclasses of the DegenerateRound class", + ) + + @classmethod + def _check_db_constraints_consistency(mcs, abci_app_cls: Type["AbciApp"]) -> None: + """Check that the pre and post conditions on the db are consistent with the initial and final states.""" + expected = abci_app_cls.initial_states + actual = abci_app_cls.db_pre_conditions.keys() + is_pre_conditions_set = len(actual) != 0 + invalid_initial_states = ( + set.difference(expected, actual) if is_pre_conditions_set else set() + ) + enforce( + len(invalid_initial_states) == 0, + f"db pre conditions contain invalid initial states: {invalid_initial_states}", + ) + expected = abci_app_cls.final_states + actual = abci_app_cls.db_post_conditions.keys() + is_post_conditions_set = len(actual) != 0 + invalid_final_states = ( + set.difference(expected, actual) if is_post_conditions_set else set() + ) + enforce( + len(invalid_final_states) == 0, + f"db post conditions contain invalid final states: {invalid_final_states}", + ) + all_pre_conditions = { + value + for values in abci_app_cls.db_pre_conditions.values() + for value in values + } + all_post_conditions = { + value + for values in abci_app_cls.db_post_conditions.values() + for value in values + } + enforce( + len(all_pre_conditions.intersection(all_post_conditions)) == 0, + "db pre and post conditions intersect", + ) + intersection = abci_app_cls.default_db_preconditions.intersection( + all_pre_conditions + ) + enforce( + len(intersection) == 0, + f"db pre conditions contain value that is a default pre condition: {intersection}", + ) + intersection = abci_app_cls.default_db_preconditions.intersection( + all_post_conditions + ) + enforce( + len(intersection) == 0, + f"db post conditions contain value that is a default post condition: {intersection}", + ) + + @classmethod + def _check_consistency_outgoing_transitions_from_non_final_states( + mcs, abci_app_cls: Type["AbciApp"] + ) -> None: + """ + Check consistency of outgoing transitions from non-final states. + + In particular, check that all non-final states have: + - at least one non-timeout transition. + - at most one timeout transition + + :param abci_app_cls: the AbciApp class + """ + states = abci_app_cls.get_all_rounds() + event_to_timeout = abci_app_cls.event_to_timeout + + non_final_states = states.difference(abci_app_cls.final_states) + timeout_events = set(event_to_timeout.keys()) + for non_final_state in non_final_states: + outgoing_transitions = abci_app_cls.transition_function[non_final_state] + + outgoing_events = set(outgoing_transitions.keys()) + outgoing_timeout_events = set.intersection(outgoing_events, timeout_events) + outgoing_nontimeout_events = set.difference(outgoing_events, timeout_events) + + enforce( + len(outgoing_timeout_events) < 2, + f"non-final state {non_final_state} cannot have more than one " + f"outgoing timeout event, got: " + f"{', '.join(map(str, outgoing_timeout_events))}", + ) + enforce( + len(outgoing_nontimeout_events) > 0, + f"non-final state {non_final_state} must have at least one " + f"non-timeout transition", + ) + + @classmethod + def _add_pending_offences_bg_round(cls, abci_app_cls: Type["AbciApp"]) -> None: + """Add the pending offences synchronization background round.""" + config: BackgroundAppConfig = BackgroundAppConfig(PendingOffencesRound) + abci_app_cls.add_background_app(config) + + +class BackgroundAppType(Enum): + """ + The type of a background app. + + Please note that the values correspond to the priority in which the background apps should be processed + when updating rounds. + """ + + TERMINATING = 0 + EVER_RUNNING = 1 + NORMAL = 2 + INCORRECT = 3 + + @staticmethod + def correct_types() -> Set[str]: + """Return the correct types only.""" + return set(BackgroundAppType.__members__) - {BackgroundAppType.INCORRECT.name} + + +@dataclass(frozen=True) +class BackgroundAppConfig(Generic[EventType]): + """ + Necessary configuration for a background app. + + For a deeper understanding of the various types of background apps and how the config influences + the generated background app's type, please refer to the `BackgroundApp` class. + The `specify_type` method provides further insight on the subject matter. + """ + + # the class of the background round + round_cls: AppState + # the abci app of the background round + # the abci app must specify a valid transition function if the round is not of an ever-running type + abci_app: Optional[Type["AbciApp"]] = None + # the start event of the background round + # if no event or transition function is specified, then the round is running in the background forever + start_event: Optional[EventType] = None + # the end event of the background round + # if not specified, then the round is terminating the abci app + end_event: Optional[EventType] = None + + +class BackgroundApp(Generic[EventType]): + """A background app.""" + + def __init__( + self, + config: BackgroundAppConfig, + ) -> None: + """Initialize the BackgroundApp.""" + given_args = locals() + + self.config = config + self.round_cls: AppState = config.round_cls + self.transition_function: Optional[AbciAppTransitionFunction] = ( + config.abci_app.transition_function if config.abci_app is not None else None + ) + self.start_event: Optional[EventType] = config.start_event + self.end_event: Optional[EventType] = config.end_event + + self.type = self.specify_type() + if self.type == BackgroundAppType.INCORRECT: # pragma: nocover + raise ValueError( + f"Background app has not been initialized correctly with {given_args}. " + f"Cannot match with any of the possible background apps' types: {BackgroundAppType.correct_types()}" + ) + _logger.debug( + f"Created background app of type '{self.type}' using {given_args}." + ) + self._background_round: Optional[AbstractRound] = None + + def __eq__(self, other: Any) -> bool: # pragma: no cover + """Custom equality comparing operator.""" + if not isinstance(other, BackgroundApp): + return False + + return self.config == other.config + + def __hash__(self) -> int: + """Custom hashing operator""" + return hash(self.config) + + def specify_type(self) -> BackgroundAppType: + """Specify the type of the background app.""" + if ( + self.start_event is None + and self.end_event is None + and self.transition_function is None + ): + self.transition_function = {} + return BackgroundAppType.EVER_RUNNING + if ( + self.start_event is not None + and self.end_event is None + and self.transition_function is not None + ): + return BackgroundAppType.TERMINATING + if ( + self.start_event is not None + and self.end_event is not None + and self.transition_function is not None + ): + return BackgroundAppType.NORMAL + return BackgroundAppType.INCORRECT # pragma: nocover + + def setup( + self, initial_synchronized_data: BaseSynchronizedData, context: SkillContext + ) -> None: + """Set up the background round.""" + round_cls = cast(Type[AbstractRound], self.round_cls) + self._background_round = round_cls( + initial_synchronized_data, + context, + ) + + @property + def background_round(self) -> AbstractRound: + """Get the background round.""" + if self._background_round is None: # pragma: nocover + raise ValueError(f"Background round with class `{self.round_cls}` not set!") + return self._background_round + + def process_transaction(self, transaction: Transaction, dry: bool = False) -> bool: + """Process a transaction.""" + + payload_class = type(transaction.payload) + bg_payload_class = cast(AppState, self.round_cls).payload_class + if payload_class is bg_payload_class: + processor = ( + self.background_round.check_transaction + if dry + else self.background_round.process_transaction + ) + processor(transaction) + return True + return False + + +@dataclass +class TransitionBackup: + """Holds transition related information as a backup in case we want to transition back from a background app.""" + + round: Optional[AbstractRound] = None + round_cls: Optional[AppState] = None + transition_function: Optional[AbciAppTransitionFunction] = None + + +class AbciApp( + Generic[EventType], ABC, metaclass=_MetaAbciApp +): # pylint: disable=too-many-instance-attributes + """ + Base class for ABCI apps. + + Concrete classes of this class implement the ABCI App. + """ + + initial_round_cls: AppState + initial_states: Set[AppState] = set() + transition_function: AbciAppTransitionFunction + final_states: Set[AppState] = set() + event_to_timeout: EventToTimeout = {} + cross_period_persisted_keys: FrozenSet[str] = frozenset() + background_apps: Set[BackgroundApp] = set() + default_db_preconditions: Set[str] = BaseSynchronizedData.default_db_keys + db_pre_conditions: Dict[AppState, Set[str]] = {} + db_post_conditions: Dict[AppState, Set[str]] = {} + _is_abstract: bool = True + + def __init__( + self, + synchronized_data: BaseSynchronizedData, + logger: logging.Logger, + context: SkillContext, + ): + """Initialize the AbciApp.""" + + synchronized_data_class = self.initial_round_cls.synchronized_data_class + synchronized_data = synchronized_data_class(db=synchronized_data.db) + + self._initial_synchronized_data = synchronized_data + self.logger = logger + self.context = context + self._current_round_cls: Optional[AppState] = None + self._current_round: Optional[AbstractRound] = None + self._last_round: Optional[AbstractRound] = None + self._previous_rounds: List[AbstractRound] = [] + self._current_round_height: int = 0 + self._round_results: List[BaseSynchronizedData] = [] + self._last_timestamp: Optional[datetime.datetime] = None + self._current_timeout_entries: List[int] = [] + self._timeouts = Timeouts[EventType]() + self._transition_backup = TransitionBackup() + self._switched = False + + @classmethod + def is_abstract(cls) -> bool: + """Return if the abci app is abstract.""" + return cls._is_abstract + + @classmethod + def add_background_app( + cls, + config: BackgroundAppConfig, + ) -> Type["AbciApp"]: + """ + Sets the background related class variables. + + For a deeper understanding of the various types of background apps and how the inputs of this method influence + the generated background app's type, please refer to the `BackgroundApp` class. + The `specify_type` method provides further insight on the subject matter. + + :param config: the background app's configuration. + :return: the `AbciApp` with the new background app contained in the `background_apps` set. + """ + background_app: BackgroundApp = BackgroundApp(config) + cls.background_apps.add(background_app) + cross_period_keys = ( + config.abci_app.cross_period_persisted_keys + if config.abci_app is not None + else frozenset() + ) + cls.cross_period_persisted_keys = cls.cross_period_persisted_keys.union( + cross_period_keys + ) + return cls + + @property + def synchronized_data(self) -> BaseSynchronizedData: + """Return the current synchronized data.""" + latest_result = self.latest_result or self._initial_synchronized_data + if self._current_round_cls is None: + return latest_result + synchronized_data_class = self._current_round_cls.synchronized_data_class + result = ( + synchronized_data_class(db=latest_result.db) + if isclass(synchronized_data_class) + and issubclass(synchronized_data_class, BaseSynchronizedData) + else latest_result + ) + return result + + @classmethod + def get_all_rounds(cls) -> Set[AppState]: + """Get all the round states.""" + return set(cls.transition_function) + + @classmethod + def get_all_events(cls) -> Set[EventType]: + """Get all the events.""" + events: Set[EventType] = set() + for _, transitions in cls.transition_function.items(): + events.update(transitions.keys()) + return events + + @staticmethod + def _get_rounds_from_transition_function( + transition_function: Optional[AbciAppTransitionFunction], + ) -> Set[AppState]: + """Get rounds from a transition function.""" + if transition_function is None: + return set() + result: Set[AppState] = set() + for start, transitions in transition_function.items(): + result.add(start) + result.update(transitions.values()) + return result + + @classmethod + def get_all_round_classes( + cls, + bg_round_cls: Set[Type[AbstractRound]], + include_background_rounds: bool = False, + ) -> Set[AppState]: + """Get all round classes.""" + full_fn = deepcopy(cls.transition_function) + + if include_background_rounds: + for app in cls.background_apps: + if ( + app.type != BackgroundAppType.EVER_RUNNING + and app.round_cls in bg_round_cls + ): + transition_fn = cast( + AbciAppTransitionFunction, app.transition_function + ) + full_fn.update(transition_fn) + + return cls._get_rounds_from_transition_function(full_fn) + + @property + def bg_apps_prioritized(self) -> Tuple[List[BackgroundApp], ...]: + """Get the background apps grouped and prioritized by their types.""" + n_correct_types = len(BackgroundAppType.correct_types()) + grouped_prioritized: Tuple[List, ...] = ([],) * n_correct_types + for app in self.background_apps: + # reminder: the values correspond to the priority of the background apps + for priority in range(n_correct_types): + if app.type == BackgroundAppType(priority): + grouped_prioritized[priority].append(app) + + return grouped_prioritized + + @property + def last_timestamp(self) -> datetime.datetime: + """Get last timestamp.""" + if self._last_timestamp is None: + raise ABCIAppInternalError("last timestamp is None") + return self._last_timestamp + + def _setup_background(self) -> None: + """Set up the background rounds.""" + for app in self.background_apps: + app.setup(self._initial_synchronized_data, self.context) + + def _get_synced_value( + self, + db_key: str, + sync_classes: Set[Type[BaseSynchronizedData]], + default: Any = None, + ) -> Any: + """Get the value of a specific database key using the synchronized data.""" + for cls in sync_classes: + # try to find the value using the synchronized data as suggested in #2131 + synced_data = cls(db=self.synchronized_data.db) + try: + res = getattr(synced_data, db_key) + except AttributeError: + # if the property does not exist in the db try the next synced data class + continue + except ValueError: + # if the property raised because of using `get_strict` and the key not being present in the db + break + + # if there is a property with the same name as the key in the db, return the result, normalized + return AbciAppDB.normalize(res) + + # as a last resort, try to get the value from the db + return self.synchronized_data.db.get(db_key, default) + + def setup(self) -> None: + """Set up the behaviour.""" + self.schedule_round(self.initial_round_cls) + self._setup_background() + # iterate through all the rounds and get all the unique synced data classes + sync_classes = { + _round.synchronized_data_class for _round in self.transition_function + } + # Add `BaseSynchronizedData` in case it does not exist (TODO: investigate and remove as it might always exist) + sync_classes.add(BaseSynchronizedData) + # set the cross-period persisted keys; avoid raising when the first period ends without a key in the db + update = { + db_key: self._get_synced_value(db_key, sync_classes) + for db_key in self.cross_period_persisted_keys + } + self.synchronized_data.db.update(**update) + + def _log_start(self) -> None: + """Log the entering in the round.""" + self.logger.info( + f"Entered in the '{self.current_round.round_id}' round for period " + f"{self.synchronized_data.period_count}" + ) + + def _log_end(self, event: EventType) -> None: + """Log the exiting from the round.""" + self.logger.info( + f"'{self.current_round.round_id}' round is done with event: {event}" + ) + + def _extend_previous_rounds_with_current_round(self) -> None: + self._previous_rounds.append(self.current_round) + self._current_round_height += 1 + + def schedule_round(self, round_cls: AppState) -> None: + """ + Schedule a round class. + + this means: + - cancel timeout events belonging to the current round; + - instantiate the new round class and set it as current round; + - create new timeout events and schedule them according to the latest + timestamp. + + :param round_cls: the class of the new round. + """ + self.logger.debug("scheduling new round: %s", round_cls) + for entry_id in self._current_timeout_entries: + self._timeouts.cancel_timeout(entry_id) + + self._current_timeout_entries = [] + next_events = list(self.transition_function.get(round_cls, {}).keys()) + for event in next_events: + timeout = self.event_to_timeout.get(event, None) + # if first round, last_timestamp is None. + # This means we do not schedule timeout events, + # but we allow timeout events from the initial state + # in case of concatenation. + if timeout is not None and self._last_timestamp is not None: + # last timestamp can be in the past relative to last seen block + # time if we're scheduling from within update_time + deadline = self.last_timestamp + datetime.timedelta(0, timeout) + entry_id = self._timeouts.add_timeout(deadline, event) + self.logger.debug( + "scheduling timeout of %s seconds for event %s with deadline %s", + timeout, + event, + deadline, + ) + self._current_timeout_entries.append(entry_id) + + self._last_round = self._current_round + self._current_round_cls = round_cls + self._current_round = round_cls( + self.synchronized_data, + self.context, + ( + self._last_round.payload_class + if self._last_round is not None + and self._last_round.payload_class + != self._current_round_cls.payload_class + # when transitioning to a round with the same payload type we set None + # as otherwise it will allow no tx to be submitted + else None + ), + ) + self._log_start() + self.synchronized_data.db.increment_round_count() # ROUND_COUNT_DEFAULT is -1 + + @property + def current_round(self) -> AbstractRound: + """Get the current round.""" + if self._current_round is None: + raise ValueError("current_round not set!") + return self._current_round + + @property + def current_round_id(self) -> Optional[str]: + """Get the current round id.""" + return self._current_round.round_id if self._current_round else None + + @property + def current_round_height(self) -> int: + """Get the current round height.""" + return self._current_round_height + + @property + def last_round_id(self) -> Optional[str]: + """Get the last round id.""" + return self._last_round.round_id if self._last_round else None + + @property + def is_finished(self) -> bool: + """Check whether the AbciApp execution has finished.""" + return self._current_round is None + + @property + def latest_result(self) -> Optional[BaseSynchronizedData]: + """Get the latest result of the round.""" + return None if len(self._round_results) == 0 else self._round_results[-1] + + def cleanup_timeouts(self) -> None: + """ + Remove all timeouts. + + Note that this is method is meant to be used only when performing recovery. + Calling it in normal execution will result in unexpected behaviour. + """ + self._timeouts = Timeouts[EventType]() + self._current_timeout_entries = [] + self._last_timestamp = None + + def check_transaction(self, transaction: Transaction) -> None: + """Check a transaction.""" + + self.process_transaction(transaction, dry=True) + + def process_transaction(self, transaction: Transaction, dry: bool = False) -> None: + """ + Process a transaction. + + The background rounds run concurrently with other (normal) rounds. + First we check if the transaction is meant for a background round, + if not we forward it to the current round object. + + :param transaction: the transaction. + :param dry: whether the transaction should only be checked and not processed. + """ + + for app in self.background_apps: + processed = app.process_transaction(transaction, dry) + if processed: + return + + processor = ( + self.current_round.check_transaction + if dry + else self.current_round.process_transaction + ) + processor(transaction) + + def _resolve_bg_transition( + self, app: BackgroundApp, event: EventType + ) -> Tuple[bool, Optional[AppState]]: + """ + Resolve a background app's transition. + + First check whether the event is a special start event. + If that's the case, proceed with the corresponding background app's transition function, + regardless of what the current round is. + + :param app: the background app instance. + :param event: the event for the transition. + :return: the new app state. + """ + + if ( + app.type in (BackgroundAppType.NORMAL, BackgroundAppType.TERMINATING) + and event == app.start_event + ): + app.transition_function = cast( + AbciAppTransitionFunction, app.transition_function + ) + app.round_cls = cast(AppState, app.round_cls) + next_round_cls = app.transition_function[app.round_cls].get(event, None) + if next_round_cls is None: # pragma: nocover + return True, None + + # we backup the current round so we can return back to normal, in case the end event is received later + self._transition_backup.round = self._current_round + self._transition_backup.round_cls = self._current_round_cls + # we switch the current transition function, with the background app's transition function + self._transition_backup.transition_function = deepcopy( + self.transition_function + ) + self.transition_function = app.transition_function + self.logger.info( + f"The {event} event was produced, transitioning to " + f"`{next_round_cls.auto_round_id()}`." + ) + return True, next_round_cls + + return False, None + + def _adjust_transition_fn(self, event: EventType) -> None: + """ + Adjust the transition function if necessary. + + Check whether the event is a special end event. + If that's the case, reset the transition function back to normal. + This method is meant to be called after resolving the next round transition, given an event. + + :param event: the emitted event. + """ + if self._transition_backup.transition_function is None: + return + + for app in self.background_apps: + if app.type == BackgroundAppType.NORMAL and event == app.end_event: + self._current_round = self._transition_backup.round + self._transition_backup.round = None + self._current_round_cls = self._transition_backup.round_cls + self._transition_backup.round_cls = None + backup_fn = cast( + AbciAppTransitionFunction, + self._transition_backup.transition_function, + ) + self.transition_function = deepcopy(backup_fn) + self._transition_backup.transition_function = None + self._switched = True + self.logger.info( + f"The {app.end_event} event was produced. Switching back to the normal FSM." + ) + + def _resolve_transition(self, event: EventType) -> Optional[Type[AbstractRound]]: + """Resolve the transitioning based on the given event.""" + for app in self.background_apps: + matched, next_round_cls = self._resolve_bg_transition(app, event) + if matched: + return next_round_cls + + self._adjust_transition_fn(event) + + current_round_cls = cast(AppState, self._current_round_cls) + next_round_cls = self.transition_function[current_round_cls].get(event, None) + if next_round_cls is None: + return None + + return next_round_cls + + def process_event( + self, event: EventType, result: Optional[BaseSynchronizedData] = None + ) -> None: + """Process a round event.""" + if self._current_round_cls is None: + self.logger.warning( + f"Cannot process event '{event}' as current state is not set" + ) + return + + next_round_cls = self._resolve_transition(event) + self._extend_previous_rounds_with_current_round() + # if there is no result, we duplicate the state since the round was preemptively ended + result = self.current_round.synchronized_data if result is None else result + self._round_results.append(result) + + self._log_end(event) + if next_round_cls is not None: + self.schedule_round(next_round_cls) + return + + if self._switched: + self._switched = False + return + + self.logger.warning("AbciApp has reached a dead end.") + self._current_round_cls = None + self._current_round = None + + def update_time(self, timestamp: datetime.datetime) -> None: + """ + Observe timestamp from last block. + + :param timestamp: the latest block's timestamp. + """ + self.logger.debug("arrived block with timestamp: %s", timestamp) + self.logger.debug("current AbciApp time: %s", self._last_timestamp) + self._timeouts.pop_earliest_cancelled_timeouts() + + if self._timeouts.size == 0: + # if no pending timeouts, then it is safe to + # move forward the last known timestamp to the + # latest block's timestamp. + self.logger.debug("no pending timeout, move time forward") + self._last_timestamp = timestamp + return + + earliest_deadline, _ = self._timeouts.get_earliest_timeout() + while earliest_deadline <= timestamp: + # the earliest deadline is expired. Pop it from the + # priority queue and process the timeout event. + expired_deadline, timeout_event = self._timeouts.pop_timeout() + self.logger.warning( + "expired deadline %s with event %s at AbciApp time %s", + expired_deadline, + timeout_event, + timestamp, + ) + + # the last timestamp now becomes the expired deadline + # clearly, it is earlier than the current highest known + # timestamp that comes from the consensus engine. + # However, we need it to correctly simulate the timeouts + # of the next rounds. (for now we set it to timestamp to explore + # the impact) + self._last_timestamp = timestamp + self.logger.warning( + "current AbciApp time after expired deadline: %s", self.last_timestamp + ) + + self.process_event(timeout_event) + + self._timeouts.pop_earliest_cancelled_timeouts() + if self._timeouts.size == 0: + break + earliest_deadline, _ = self._timeouts.get_earliest_timeout() + + # at this point, there is no timeout event left to be triggered, + # so it is safe to move forward the last known timestamp to the + # new block's timestamp + self._last_timestamp = timestamp + self.logger.debug("final AbciApp time: %s", self._last_timestamp) + + def cleanup( + self, + cleanup_history_depth: int, + cleanup_history_depth_current: Optional[int] = None, + ) -> None: + """Clear data.""" + if len(self._round_results) != len(self._previous_rounds): + raise ABCIAppInternalError("Inconsistent round lengths") # pragma: nocover + # we need at least the last round result, and for symmetry we impose the same condition + # on previous rounds and state.db + cleanup_history_depth = max(cleanup_history_depth, MIN_HISTORY_DEPTH) + self._previous_rounds = self._previous_rounds[-cleanup_history_depth:] + self._round_results = self._round_results[-cleanup_history_depth:] + self.synchronized_data.db.cleanup( + cleanup_history_depth, cleanup_history_depth_current + ) + + def cleanup_current_histories(self, cleanup_history_depth_current: int) -> None: + """Reset the parameter histories for the current entry (period), keeping only the latest values for each parameter.""" + self.synchronized_data.db.cleanup_current_histories( + cleanup_history_depth_current + ) + + +class OffenseType(Enum): + """ + The types of offenses. + + The values of the enum represent the seriousness of the offence. + Offense types with values >1000 are considered serious. + See also `is_light_offence` and `is_serious_offence` functions. + """ + + NO_OFFENCE = -2 + CUSTOM = -1 + VALIDATOR_DOWNTIME = 0 + INVALID_PAYLOAD = 1 + BLACKLISTED = 2 + SUSPECTED = 3 + UNKNOWN = SERIOUS_OFFENCE_ENUM_MIN + DOUBLE_SIGNING = SERIOUS_OFFENCE_ENUM_MIN + 1 + LIGHT_CLIENT_ATTACK = SERIOUS_OFFENCE_ENUM_MIN + 2 + + +def is_light_offence(offence_type: OffenseType) -> bool: + """Check if an offence type is light.""" + return 0 <= offence_type.value < SERIOUS_OFFENCE_ENUM_MIN + + +def is_serious_offence(offence_type: OffenseType) -> bool: + """Check if an offence type is serious.""" + return offence_type.value >= SERIOUS_OFFENCE_ENUM_MIN + + +def light_offences() -> Iterator[OffenseType]: + """Get the light offences.""" + return filter(is_light_offence, OffenseType) + + +def serious_offences() -> Iterator[OffenseType]: + """Get the serious offences.""" + return filter(is_serious_offence, OffenseType) + + +class AvailabilityWindow: + """ + A cyclic array with a maximum length that holds boolean values. + + When an element is added to the array and the maximum length has been reached, + the oldest element is removed. Two attributes `num_positive` and `num_negative` + reflect the number of positive and negative elements in the AvailabilityWindow, + they are updated every time a new element is added. + """ + + def __init__(self, max_length: int) -> None: + """ + Initializes the `AvailabilityWindow` instance. + + :param max_length: the maximum length of the cyclic array. + """ + if max_length < 1: + raise ValueError( + f"An `AvailabilityWindow` with a `max_length` {max_length} < 1 is not valid." + ) + + self._max_length = max_length + self._window: Deque[bool] = deque(maxlen=max_length) + self._num_positive = 0 + self._num_negative = 0 + + def __eq__(self, other: Any) -> bool: + """Compare `AvailabilityWindow` objects.""" + if isinstance(other, AvailabilityWindow): + return self.to_dict() == other.to_dict() + return False + + def has_bad_availability_rate(self, threshold: float = 0.95) -> bool: + """Whether the agent on which the window belongs to has a bad availability rate or not.""" + return self._num_positive >= ceil(self._max_length * threshold) + + def _update_counters(self, positive: bool, removal: bool = False) -> None: + """Updates the `num_positive` and `num_negative` counters.""" + update_amount = -1 if removal else 1 + + if positive: + if self._num_positive == 0 and update_amount == -1: # pragma: no cover + return + self._num_positive += update_amount + else: + if self._num_negative == 0 and update_amount == -1: # pragma: no cover + return + self._num_negative += update_amount + + def add(self, value: bool) -> None: + """ + Adds a new boolean value to the cyclic array. + + If the maximum length has been reached, the oldest element is removed. + + :param value: The boolean value to add to the cyclic array. + """ + if len(self._window) == self._max_length and self._max_length > 0: + # we have filled the window, we need to pop the oldest element + # and update the score accordingly + oldest_value = self._window.popleft() + self._update_counters(oldest_value, removal=True) + + self._window.append(value) + self._update_counters(value) + + def to_dict(self) -> Dict[str, int]: + """Returns a dictionary representation of the `AvailabilityWindow` instance.""" + return { + "max_length": self._max_length, + # Please note that the value cannot be represented if the max length of the availability window is > 14_285 + "array": ( + int("".join(str(int(flag)) for flag in self._window), base=2) + if len(self._window) + else 0 + ), + "num_positive": self._num_positive, + "num_negative": self._num_negative, + } + + @staticmethod + def _validate_key( + data: Dict[str, int], key: str, validator: Callable[[int], bool] + ) -> None: + """Validate the given key in the data.""" + value = data.get(key, None) + if value is None: + raise ValueError(f"Missing required key: {key}.") + + if not isinstance(value, int): + raise ValueError(f"{key} must be of type int.") + + if not validator(value): + raise ValueError(f"{key} has invalid value {value}.") + + @staticmethod + def _validate(data: Dict[str, int]) -> None: + """Check if the input can be properly mapped to the class attributes.""" + if not isinstance(data, dict): + raise TypeError(f"Expected dict, got {type(data)}") + + attribute_to_validator = { + "max_length": lambda x: x > 0, + "array": lambda x: 0 <= x < 2 ** data["max_length"], + "num_positive": lambda x: x >= 0, + "num_negative": lambda x: x >= 0, + } + + errors = [] + for attribute, validator in attribute_to_validator.items(): + try: + AvailabilityWindow._validate_key(data, attribute, validator) + except ValueError as e: + errors.append(str(e)) + + if errors: + raise ValueError("Invalid input:\n" + "\n".join(errors)) + + @classmethod + def from_dict(cls, data: Dict[str, int]) -> "AvailabilityWindow": + """Initializes an `AvailabilityWindow` instance from a dictionary.""" + cls._validate(data) + + # convert the serialized array to a binary string + binary_number = bin(data["array"])[2:] + # convert each character in the binary string to a flag + flags = (bool(int(digit)) for digit in binary_number) + + instance = cls(max_length=data["max_length"]) + instance._window.extend(flags) + instance._num_positive = data["num_positive"] + instance._num_negative = data["num_negative"] + return instance + + +@dataclass +class OffenceStatus: + """A class that holds information about offence status for an agent.""" + + validator_downtime: AvailabilityWindow = field( + default_factory=lambda: AvailabilityWindow(NUMBER_OF_BLOCKS_TRACKED) + ) + invalid_payload: AvailabilityWindow = field( + default_factory=lambda: AvailabilityWindow(NUMBER_OF_ROUNDS_TRACKED) + ) + blacklisted: AvailabilityWindow = field( + default_factory=lambda: AvailabilityWindow(NUMBER_OF_ROUNDS_TRACKED) + ) + suspected: AvailabilityWindow = field( + default_factory=lambda: AvailabilityWindow(NUMBER_OF_ROUNDS_TRACKED) + ) + num_unknown_offenses: int = 0 + num_double_signed: int = 0 + num_light_client_attack: int = 0 + custom_offences_amount: int = 0 + + def slash_amount(self, light_unit_amount: int, serious_unit_amount: int) -> int: + """Get the slash amount of the current status.""" + offence_types = [] + + if self.validator_downtime.has_bad_availability_rate(): + offence_types.append(OffenseType.VALIDATOR_DOWNTIME) + if self.invalid_payload.has_bad_availability_rate(): + offence_types.append(OffenseType.INVALID_PAYLOAD) + if self.blacklisted.has_bad_availability_rate(): + offence_types.append(OffenseType.BLACKLISTED) + if self.suspected.has_bad_availability_rate(): + offence_types.append(OffenseType.SUSPECTED) + offence_types.extend([OffenseType.UNKNOWN] * self.num_unknown_offenses) + offence_types.extend([OffenseType.UNKNOWN] * self.num_double_signed) + offence_types.extend([OffenseType.UNKNOWN] * self.num_light_client_attack) + + light_multiplier = 0 + serious_multiplier = 0 + for offence_type in offence_types: + light_multiplier += bool(is_light_offence(offence_type)) + serious_multiplier += bool(is_serious_offence(offence_type)) + + return ( + light_multiplier * light_unit_amount + + serious_multiplier * serious_unit_amount + + self.custom_offences_amount + ) + + +class OffenseStatusEncoder(json.JSONEncoder): + """A custom JSON encoder for the offence status dictionary.""" + + def default(self, o: Any) -> Any: + """The default JSON encoder.""" + if is_dataclass(o): + return asdict(o) + if isinstance(o, AvailabilityWindow): + return o.to_dict() + return super().default(o) + + +class OffenseStatusDecoder(json.JSONDecoder): + """A custom JSON decoder for the offence status dictionary.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize the custom JSON decoder.""" + super().__init__(object_hook=self.hook, *args, **kwargs) + + @staticmethod + def hook( + data: Dict[str, Any] + ) -> Union[AvailabilityWindow, OffenceStatus, Dict[str, OffenceStatus]]: + """Perform the custom decoding.""" + # if this is an `AvailabilityWindow` + window_attributes = sorted(AvailabilityWindow(1).to_dict().keys()) + if window_attributes == sorted(data.keys()): + return AvailabilityWindow.from_dict(data) + + # if this is an `OffenceStatus` + status_attributes = ( + OffenceStatus.__annotations__.keys() # pylint: disable=no-member + ) + if sorted(status_attributes) == sorted(data.keys()): + return OffenceStatus(**data) + + return data + + +@dataclass(frozen=True, eq=True) +class PendingOffense: + """A dataclass to represent offences that need to be addressed.""" + + accused_agent_address: str + round_count: int + offense_type: OffenseType + last_transition_timestamp: float + time_to_live: float + # only takes effect if the `OffenseType` is of type `CUSTOM`, otherwise it is ignored + custom_amount: int = 0 + + def __post_init__(self) -> None: + """Post initialization for offence type conversion in case it is given as an `int`.""" + if isinstance(self.offense_type, int): + super().__setattr__("offense_type", OffenseType(self.offense_type)) + + +class SlashingNotConfiguredError(Exception): + """Custom exception raised when slashing configuration is requested but is not available.""" + + +DEFAULT_PENDING_OFFENCE_TTL = 2 * 60 * 60 # 1 hour + + +class RoundSequence: # pylint: disable=too-many-instance-attributes + """ + This class represents a sequence of rounds + + It is a generic class that keeps track of the current round + of the consensus period. It receives 'deliver_tx' requests + from the ABCI handlers and forwards them to the current + active round instance, which implements the ABCI app logic. + It also schedules the next round (if any) whenever a round terminates. + """ + + class _BlockConstructionState(Enum): + """ + Phases of an ABCI-based block construction. + + WAITING_FOR_BEGIN_BLOCK: the app is ready to accept + "begin_block" requests from the consensus engine node. + Then, it transitions into the 'WAITING_FOR_DELIVER_TX' phase. + WAITING_FOR_DELIVER_TX: the app is building the block + by accepting "deliver_tx" requests, and waits + until the "end_block" request. + Then, it transitions into the 'WAITING_FOR_COMMIT' phase. + WAITING_FOR_COMMIT: the app finished the construction + of the block, but it is waiting for the "commit" + request from the consensus engine node. + Then, it transitions into the 'WAITING_FOR_BEGIN_BLOCK' phase. + """ + + WAITING_FOR_BEGIN_BLOCK = "waiting_for_begin_block" + WAITING_FOR_DELIVER_TX = "waiting_for_deliver_tx" + WAITING_FOR_COMMIT = "waiting_for_commit" + + def __init__(self, context: SkillContext, abci_app_cls: Type[AbciApp]): + """Initialize the round.""" + self._blockchain = Blockchain() + self._syncing_up = True + self._context = context + self._block_construction_phase = ( + RoundSequence._BlockConstructionState.WAITING_FOR_BEGIN_BLOCK + ) + + self._block_builder = BlockBuilder() + self._abci_app_cls = abci_app_cls + self._abci_app: Optional[AbciApp] = None + self._last_round_transition_timestamp: Optional[datetime.datetime] = None + self._last_round_transition_height = 0 + self._last_round_transition_root_hash = b"" + self._last_round_transition_tm_height: Optional[int] = None + self._tm_height: Optional[int] = None + self._block_stall_deadline: Optional[datetime.datetime] = None + self._terminating_round_called: bool = False + # a mapping of the validators' addresses to their agent addresses + # we create a mapping to avoid calculating the agent address from the validator address every time we need it + # since this is an operation that will be performed every time we want to create an offence + self._validator_to_agent: Dict[str, str] = {} + # a mapping of the agents' addresses to their offence status + self._offence_status: Dict[str, OffenceStatus] = {} + self._slashing_enabled = False + self.pending_offences: Set[PendingOffense] = set() + + def enable_slashing(self) -> None: + """Enable slashing.""" + self._slashing_enabled = True + + @property + def validator_to_agent(self) -> Dict[str, str]: + """Get the mapping of the validators' addresses to their agent addresses.""" + if self._validator_to_agent: + return self._validator_to_agent + raise SlashingNotConfiguredError( + "The mapping of the validators' addresses to their agent addresses has not been set." + ) + + @validator_to_agent.setter + def validator_to_agent(self, validator_to_agent: Dict[str, str]) -> None: + """Set the mapping of the validators' addresses to their agent addresses.""" + if self._validator_to_agent: + raise ValueError( + "The mapping of the validators' addresses to their agent addresses can only be set once. " + f"Attempted to set with {validator_to_agent} but it has content already: {self._validator_to_agent}." + ) + self._validator_to_agent = validator_to_agent + + @property + def offence_status(self) -> Dict[str, OffenceStatus]: + """Get the mapping of the agents' addresses to their offence status.""" + if self._offence_status: + return self._offence_status + raise SlashingNotConfiguredError( # pragma: nocover + "The mapping of the agents' addresses to their offence status has not been set." + ) + + @offence_status.setter + def offence_status(self, offence_status: Dict[str, OffenceStatus]) -> None: + """Set the mapping of the agents' addresses to their offence status.""" + self.abci_app.logger.debug(f"Setting offence status to: {offence_status}") + self._offence_status = offence_status + self.store_offence_status() + + def add_pending_offence(self, pending_offence: PendingOffense) -> None: + """ + Add a pending offence to the set of pending offences. + + Pending offences are offences that have been detected, but not yet agreed upon by the consensus. + A pending offence is removed from the set of pending offences and added to the OffenceStatus of a validator + when the majority of the agents agree on it. + + :param pending_offence: the pending offence to add + :return: None + """ + self.pending_offences.add(pending_offence) + + def sync_db_and_slashing(self, serialized_db_state: str) -> None: + """Sync the database and the slashing configuration.""" + self.abci_app.synchronized_data.db.sync(serialized_db_state) + offence_status = self.latest_synchronized_data.slashing_config + if offence_status: + # deserialize the offence status and load it to memory + self.offence_status = json.loads( + offence_status, + cls=OffenseStatusDecoder, + ) + + def serialized_offence_status(self) -> str: + """Serialize the offence status.""" + return json.dumps(self.offence_status, cls=OffenseStatusEncoder, sort_keys=True) + + def store_offence_status(self) -> None: + """Store the serialized offence status.""" + if not self._slashing_enabled: + # if slashing is not enabled, we do not update anything + return + encoded_status = self.serialized_offence_status() + self.latest_synchronized_data.slashing_config = encoded_status + self.abci_app.logger.debug(f"Updated db with: {encoded_status}") + self.abci_app.logger.debug(f"App hash now is: {self.root_hash.hex()}") + + def get_agent_address(self, validator: Validator) -> str: + """Get corresponding agent address from a `Validator` instance.""" + validator_address = validator.address.hex().upper() + + try: + return self.validator_to_agent[validator_address] + except KeyError as exc: + raise ValueError( + f"Requested agent address for an unknown validator address {validator_address}. " + f"Available validators are: {self.validator_to_agent.keys()}" + ) from exc + + def setup(self, *args: Any, **kwargs: Any) -> None: + """ + Set up the round sequence. + + :param args: the arguments to pass to the round constructor. + :param kwargs: the keyword-arguments to pass to the round constructor. + """ + kwargs["context"] = self._context + self._abci_app = self._abci_app_cls(*args, **kwargs) + self._abci_app.setup() + + def start_sync( + self, + ) -> None: # pragma: nocover + """ + Set `_syncing_up` flag to true. + + if the _syncing_up flag is set to true, the `async_act` method won't be executed. For more details refer to + https://github.com/valory-xyz/open-autonomy/issues/247#issuecomment-1012268656 + """ + self._syncing_up = True + + def end_sync( + self, + ) -> None: + """Set `_syncing_up` flag to false.""" + self._syncing_up = False + + @property + def syncing_up( + self, + ) -> bool: + """Return if the app is in sync mode.""" + return self._syncing_up + + @property + def abci_app(self) -> AbciApp: + """Get the AbciApp.""" + if self._abci_app is None: + raise ABCIAppInternalError("AbciApp not set") # pragma: nocover + return self._abci_app + + @property + def blockchain(self) -> Blockchain: + """Get the Blockchain instance.""" + return self._blockchain + + @blockchain.setter + def blockchain(self, _blockchain: Blockchain) -> None: + """Get the Blockchain instance.""" + self._blockchain = _blockchain + + @property + def height(self) -> int: + """Get the height.""" + return self._blockchain.height + + @property + def is_finished(self) -> bool: + """Check if a round sequence has finished.""" + return self.abci_app.is_finished + + def check_is_finished(self) -> None: + """Check if a round sequence has finished.""" + if self.is_finished: + raise ValueError( + "round sequence is finished, cannot accept new transactions" + ) + + @property + def current_round(self) -> AbstractRound: + """Get current round.""" + return self.abci_app.current_round + + @property + def current_round_id(self) -> Optional[str]: + """Get the current round id.""" + return self.abci_app.current_round_id + + @property + def current_round_height(self) -> int: + """Get the current round height.""" + return self.abci_app.current_round_height + + @property + def last_round_id(self) -> Optional[str]: + """Get the last round id.""" + return self.abci_app.last_round_id + + @property + def last_timestamp(self) -> datetime.datetime: + """Get the last timestamp.""" + last_timestamp = ( + self._blockchain.blocks[-1].timestamp + if self._blockchain.length != 0 + else None + ) + if last_timestamp is None: + raise ABCIAppInternalError("last timestamp is None") + return last_timestamp + + @property + def last_round_transition_timestamp( + self, + ) -> datetime.datetime: + """Returns the timestamp for last round transition.""" + if self._last_round_transition_timestamp is None: + raise ValueError( + "Trying to access `last_round_transition_timestamp` while no transition has been completed yet." + ) + + return self._last_round_transition_timestamp + + @property + def last_round_transition_height( + self, + ) -> int: + """Returns the height for last round transition.""" + if self._last_round_transition_height == 0: + raise ValueError( + "Trying to access `last_round_transition_height` while no transition has been completed yet." + ) + + return self._last_round_transition_height + + @property + def last_round_transition_root_hash( + self, + ) -> bytes: + """Returns the root hash for last round transition.""" + if self._last_round_transition_root_hash == b"": + # if called for the first chain initialization, return the hash resulting from the initial abci app's state + return self.root_hash + return self._last_round_transition_root_hash + + @property + def last_round_transition_tm_height(self) -> int: + """Returns the Tendermint height for last round transition.""" + if self._last_round_transition_tm_height is None: + raise ValueError( + "Trying to access Tendermint's last round transition height before any `end_block` calls." + ) + return self._last_round_transition_tm_height + + @property + def latest_synchronized_data(self) -> BaseSynchronizedData: + """Get the latest synchronized_data.""" + return self.abci_app.synchronized_data + + @property + def root_hash(self) -> bytes: + """ + Get the Merkle root hash of the application state. + + This is going to be the database's hash. + In this way, the app hash will be reflecting our application's state, + and will guarantee that all the agents on the chain apply the changes of the arriving blocks in the same way. + + :return: the root hash to be included as the Header.AppHash in the next block. + """ + return self.abci_app.synchronized_data.db.hash() + + @property + def tm_height(self) -> int: + """Get Tendermint's current height.""" + if self._tm_height is None: + raise ValueError( + "Trying to access Tendermint's current height before any `end_block` calls." + ) + return self._tm_height + + @tm_height.setter + def tm_height(self, _tm_height: int) -> None: + """Set Tendermint's current height.""" + self._tm_height = _tm_height + + @property + def block_stall_deadline_expired(self) -> bool: + """Get if the deadline for not having received any begin block requests from the Tendermint node has expired.""" + if self._block_stall_deadline is None: + return False + return datetime.datetime.now() > self._block_stall_deadline + + def set_block_stall_deadline(self) -> None: + """Use the local time of the agent and a predefined tolerance, to specify the expiration of the deadline.""" + self._block_stall_deadline = datetime.datetime.now() + datetime.timedelta( + seconds=BLOCKS_STALL_TOLERANCE + ) + + def init_chain(self, initial_height: int) -> None: + """Init chain.""" + # reduce `initial_height` by 1 to get block count offset as per Tendermint protocol + self._blockchain = Blockchain(initial_height - 1) + + def _track_tm_offences( + self, evidences: Evidences, last_commit_info: LastCommitInfo + ) -> None: + """Track offences provided by Tendermint, if there are any.""" + for vote_info in last_commit_info.votes: + agent_address = self.get_agent_address(vote_info.validator) + was_down = not vote_info.signed_last_block + self.offence_status[agent_address].validator_downtime.add(was_down) + + for byzantine_validator in evidences.byzantine_validators: + agent_address = self.get_agent_address(byzantine_validator.validator) + evidence_type = byzantine_validator.evidence_type + self.offence_status[agent_address].num_unknown_offenses += bool( + evidence_type == EvidenceType.UNKNOWN + ) + self.offence_status[agent_address].num_double_signed += bool( + evidence_type == EvidenceType.DUPLICATE_VOTE + ) + self.offence_status[agent_address].num_light_client_attack += bool( + evidence_type == EvidenceType.LIGHT_CLIENT_ATTACK + ) + + def _track_app_offences(self) -> None: + """Track offences provided by the app level, if there are any.""" + synced_data = self.abci_app.synchronized_data + for agent in self.offence_status.keys(): + blacklisted = agent in synced_data.blacklisted_keepers + suspected = agent in cast(tuple, synced_data.db.get("suspects", tuple())) + agent_status = self.offence_status[agent] + agent_status.blacklisted.add(blacklisted) + agent_status.suspected.add(suspected) + + def _handle_slashing_not_configured(self, exc: SlashingNotConfiguredError) -> None: + """Handle a `SlashingNotConfiguredError`.""" + # In the current slashing implementation, we do not track offences before setting the slashing + # configuration, i.e., before successfully sharing the tm configuration via ACN on registration. + # That is because we cannot slash an agent if we do not map their validator address to their agent address. + # Checking the number of participants will allow us to identify whether the registration round has finished, + # and therefore expect that the slashing configuration has been set if ACN registration is enabled. + if self.abci_app.synchronized_data.nb_participants: + _logger.error( + f"{exc} This error may occur when the ACN registration has not been successfully performed. " + "Have you set the `share_tm_config_on_startup` flag to `true` in the configuration?" + ) + self._slashing_enabled = False + _logger.warning("Slashing has been disabled!") + + def _try_track_offences( + self, evidences: Evidences, last_commit_info: LastCommitInfo + ) -> None: + """Try to track the offences. If an error occurs, log it, disable slashing, and warn about the latter.""" + try: + if self._slashing_enabled: + # only track offences if the first round has finished + # we avoid tracking offences in the first round + # because we do not have the slashing configuration synced yet + self._track_tm_offences(evidences, last_commit_info) + self._track_app_offences() + except SlashingNotConfiguredError as exc: + self._handle_slashing_not_configured(exc) + + def begin_block( + self, + header: Header, + evidences: Evidences, + last_commit_info: LastCommitInfo, + ) -> None: + """Begin block.""" + if self.is_finished: + raise ABCIAppInternalError( + "round sequence is finished, cannot accept new blocks" + ) + if ( + self._block_construction_phase + != RoundSequence._BlockConstructionState.WAITING_FOR_BEGIN_BLOCK + ): + raise ABCIAppInternalError( + f"cannot accept a 'begin_block' request. Current phase={self._block_construction_phase}" + ) + + # From now on, the ABCI app waits for 'deliver_tx' requests, until 'end_block' is received + self._block_construction_phase = ( + RoundSequence._BlockConstructionState.WAITING_FOR_DELIVER_TX + ) + self._block_builder.reset() + self._block_builder.header = header + self.abci_app.update_time(header.timestamp) + self.set_block_stall_deadline() + self.abci_app.logger.debug( + "Created a new local deadline for the next `begin_block` request from the Tendermint node: " + f"{self._block_stall_deadline}" + ) + self._try_track_offences(evidences, last_commit_info) + + def deliver_tx(self, transaction: Transaction) -> None: + """ + Deliver a transaction. + + Appends the transaction to build the block on 'end_block' later. + :param transaction: the transaction. + :raises: an Error otherwise. + """ + if ( + self._block_construction_phase + != RoundSequence._BlockConstructionState.WAITING_FOR_DELIVER_TX + ): + raise ABCIAppInternalError( + f"cannot accept a 'deliver_tx' request. Current phase={self._block_construction_phase}" + ) + + self.abci_app.check_transaction(transaction) + self.abci_app.process_transaction(transaction) + self._block_builder.add_transaction(transaction) + + def end_block(self) -> None: + """Process the 'end_block' request.""" + if ( + self._block_construction_phase + != RoundSequence._BlockConstructionState.WAITING_FOR_DELIVER_TX + ): + raise ABCIAppInternalError( + f"cannot accept a 'end_block' request. Current phase={self._block_construction_phase}" + ) + # The ABCI app waits for the commit + self._block_construction_phase = ( + RoundSequence._BlockConstructionState.WAITING_FOR_COMMIT + ) + + def commit(self) -> None: + """Process the 'commit' request.""" + if ( + self._block_construction_phase + != RoundSequence._BlockConstructionState.WAITING_FOR_COMMIT + ): + raise ABCIAppInternalError( + f"cannot accept a 'commit' request. Current phase={self._block_construction_phase}" + ) + block = self._block_builder.get_block() + try: + if self._blockchain.is_init: + # There are occasions where we wait for an init_chain() before accepting blocks. + # This can happen during hard reset, where we might've reset the local blockchain, + # But are still receiving requests from the not yet reset tendermint node. + # We only process blocks on an initialized local blockchain. + # The local blockchain gets initialized upon receiving an init_chain request from + # the tendermint node. In cases where we don't want to wait for the init_chain req, + # one can create a Blockchain instance with `is_init=True`, i.e. the default args. + self._blockchain.add_block(block) + self._update_round() + else: + self.abci_app.logger.warning( + f"Received block with height {block.header.height} before the blockchain was initialized." + ) + # The ABCI app now waits again for the next block + self._block_construction_phase = ( + RoundSequence._BlockConstructionState.WAITING_FOR_BEGIN_BLOCK + ) + except AddBlockError as exception: + raise exception + + def reset_blockchain(self, is_replay: bool = False, is_init: bool = False) -> None: + """ + Reset blockchain after tendermint reset. + + :param is_replay: whether we are resetting the blockchain while replaying blocks. + :param is_init: whether to process blocks before receiving an init_chain req from tendermint. + """ + if is_replay: + self._block_construction_phase = ( + RoundSequence._BlockConstructionState.WAITING_FOR_BEGIN_BLOCK + ) + self._blockchain = Blockchain(is_init=is_init) + + def _get_round_result( + self, + ) -> Optional[Tuple[BaseSynchronizedData, Any]]: + """ + Get the round's result. + + Give priority to: + 1. terminating bg rounds + 2. ever running bg rounds + 3. normal bg rounds + 4. normal rounds + + :return: the round's result. + """ + for prioritized_group in self.abci_app.bg_apps_prioritized: + for app in prioritized_group: + result = app.background_round.end_block() + if ( + result is None + or app.type == BackgroundAppType.TERMINATING + and self._terminating_round_called + ): + continue + if ( + app.type == BackgroundAppType.TERMINATING + and not self._terminating_round_called + ): + self._terminating_round_called = True + return result + return self.abci_app.current_round.end_block() + + def _update_round(self) -> None: + """ + Update a round. + + Check whether the round has finished. If so, get the new round and set it as the current round. + If a termination app's round has returned a result, then the other apps' rounds are ignored. + """ + result = self._get_round_result() + + if result is None: + # neither the background rounds, nor the current round returned, so no update needs to be made + return + + # update the offence status at the end of each round + # this is done to ensure that the offence status is always up-to-date & in sync + # the next step is a no-op if slashing is not enabled + self.store_offence_status() + + self._last_round_transition_timestamp = self._blockchain.last_block.timestamp + self._last_round_transition_height = self.height + self._last_round_transition_root_hash = self.root_hash + self._last_round_transition_tm_height = self.tm_height + + round_result, event = result + self.abci_app.logger.debug( + f"updating round, current_round {self.current_round.round_id}, event: {event}, round result {round_result}" + ) + self.abci_app.process_event(event, result=round_result) + + def _reset_to_default_params(self) -> None: + """Resets the instance params to their default value.""" + self._last_round_transition_timestamp = None + self._last_round_transition_height = 0 + self._last_round_transition_root_hash = b"" + self._last_round_transition_tm_height = None + self._tm_height = None + self._slashing_enabled = False + self.pending_offences = set() + + def reset_state( + self, + restart_from_round: str, + round_count: int, + serialized_db_state: Optional[str] = None, + ) -> None: + """ + This method resets the state of RoundSequence to the beginning of the period. + + Note: This is intended to be used for agent <-> tendermint communication recovery only! + + :param restart_from_round: from which round to restart the abci. + This round should be the first round in the last period. + :param round_count: the round count at the beginning of the period -1. + :param serialized_db_state: the state of the database at the beginning of the period. + If provided, the database will be reset to this state. + """ + self._reset_to_default_params() + self.abci_app.synchronized_data.db.round_count = round_count + if serialized_db_state is not None: + self.sync_db_and_slashing(serialized_db_state) + # Furthermore, that hash is then in turn used as the init hash when the tm network is reset. + self._last_round_transition_root_hash = self.root_hash + + self.abci_app.cleanup_timeouts() + round_id_to_cls = { + cls.auto_round_id(): cls for cls in self.abci_app.transition_function + } + restart_from_round_cls = round_id_to_cls.get(restart_from_round, None) + if restart_from_round_cls is None: + raise ABCIAppInternalError( + "Cannot reset state. The Tendermint recovery parameters are incorrect. " + "Did you update the `restart_from_round` with an incorrect round id? " + f"Found {restart_from_round}, but the app's transition function has the following round ids: " + f"{set(round_id_to_cls.keys())}." + ) + self.abci_app.schedule_round(restart_from_round_cls) + + +@dataclass(frozen=True) +class PendingOffencesPayload(BaseTxPayload): + """Represent a transaction payload for pending offences.""" + + accused_agent_address: str + offense_round: int + offense_type_value: int + last_transition_timestamp: float + time_to_live: float + custom_amount: int + + +class PendingOffencesRound(CollectSameUntilThresholdRound): + """Defines the pending offences background round, which runs concurrently with other rounds to sync the offences.""" + + payload_class = PendingOffencesPayload + synchronized_data_class = BaseSynchronizedData + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize the `PendingOffencesRound`.""" + super().__init__(*args, **kwargs) + self._latest_round_processed = -1 + + @property + def offence_status(self) -> Dict[str, OffenceStatus]: + """Get the offence status from the round sequence.""" + return self.context.state.round_sequence.offence_status + + def end_block(self) -> None: + """ + Process the end of the block for the pending offences background round. + + It is important to note that this is a non-standard type of round, meaning it does not emit any events. + Instead, it continuously runs in the background. + The objective of this round is to consistently monitor the received pending offences + and achieve a consensus among the agents. + """ + if not self.threshold_reached: + return + + offence = PendingOffense(*self.most_voted_payload_values) + + # an offence should only be tracked once, not every time a payload is processed after the threshold is reached + if self._latest_round_processed == offence.round_count: + return + + # add synchronized offence to the offence status + # only `INVALID_PAYLOAD` offence types are supported at the moment as pending offences: + # https://github.com/valory-xyz/open-autonomy/blob/6831d6ebaf10ea8e3e04624b694c7f59a6d05bb4/packages/valory/skills/abstract_round_abci/handlers.py#L215-L222 # noqa + invalid = offence.offense_type == OffenseType.INVALID_PAYLOAD + self.offence_status[offence.accused_agent_address].invalid_payload.add(invalid) + + # if the offence is of custom type, then add the custom amount to it + if offence.offense_type == OffenseType.CUSTOM: + self.offence_status[ + offence.accused_agent_address + ].custom_offences_amount += offence.custom_amount + elif offence.custom_amount != 0: + self.context.logger.warning( + f"Custom amount for {offence=} will not take effect as it is not of `CUSTOM` type." + ) + + self._latest_round_processed = offence.round_count diff --git a/packages/valory/skills/abstract_round_abci/behaviour_utils.py b/packages/valory/skills/abstract_round_abci/behaviour_utils.py new file mode 100644 index 0000000..43f9298 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/behaviour_utils.py @@ -0,0 +1,2356 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains helper classes for behaviours.""" + + +import datetime +import inspect +import json +import pprint +import re +import sys +from abc import ABC, ABCMeta, abstractmethod +from enum import Enum +from functools import partial +from typing import ( + Any, + Callable, + Dict, + Generator, + List, + Optional, + Tuple, + Type, + Union, + cast, +) + +import pytz +from aea.exceptions import enforce +from aea.mail.base import EnvelopeContext +from aea.protocols.base import Message +from aea.protocols.dialogue.base import Dialogue +from aea.skills.behaviours import SimpleBehaviour + +from packages.open_aea.protocols.signing import SigningMessage +from packages.open_aea.protocols.signing.custom_types import ( + RawMessage, + RawTransaction, + SignedTransaction, + Terms, +) +from packages.valory.connections.http_client.connection import ( + PUBLIC_ID as HTTP_CLIENT_PUBLIC_ID, +) +from packages.valory.connections.ipfs.connection import PUBLIC_ID as IPFS_CONNECTION_ID +from packages.valory.connections.p2p_libp2p_client.connection import ( + PUBLIC_ID as P2P_LIBP2P_CLIENT_PUBLIC_ID, +) +from packages.valory.contracts.service_registry.contract import ( # noqa: F401 # pylint: disable=unused-import + ServiceRegistryContract, +) +from packages.valory.protocols.contract_api import ContractApiMessage +from packages.valory.protocols.http import HttpMessage +from packages.valory.protocols.ipfs import IpfsMessage +from packages.valory.protocols.ipfs.dialogues import IpfsDialogue, IpfsDialogues +from packages.valory.protocols.ledger_api import LedgerApiMessage +from packages.valory.protocols.tendermint import TendermintMessage +from packages.valory.skills.abstract_round_abci.base import ( + AbstractRound, + BaseSynchronizedData, + BaseTxPayload, + LEDGER_API_ADDRESS, + OK_CODE, + RoundSequence, + Transaction, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + ContractApiDialogue, + ContractApiDialogues, + HttpDialogue, + HttpDialogues, + LedgerApiDialogue, + LedgerApiDialogues, + SigningDialogues, + TendermintDialogues, +) +from packages.valory.skills.abstract_round_abci.io_.ipfs import ( + IPFSInteract, + IPFSInteractionError, +) +from packages.valory.skills.abstract_round_abci.io_.load import CustomLoaderType, Loader +from packages.valory.skills.abstract_round_abci.io_.store import ( + CustomStorerType, + Storer, + SupportedFiletype, + SupportedObjectType, +) +from packages.valory.skills.abstract_round_abci.models import ( + BaseParams, + Requests, + SharedState, + TendermintRecoveryParams, +) + + +# TODO: port registration code from registration_abci to here + + +NON_200_RETURN_CODE_DURING_RESET_THRESHOLD = 3 +GENESIS_TIME_FMT = "%Y-%m-%dT%H:%M:%S.%fZ" +INITIAL_APP_HASH = "" +INITIAL_HEIGHT = "0" +TM_REQ_TIMEOUT = 5 # 5 seconds +FLASHBOTS_LEDGER_ID = "ethereum_flashbots" +SOLANA_LEDGER_ID = "solana" + + +class SendException(Exception): + """Exception raised if the 'try_send' to an AsyncBehaviour failed.""" + + +class TimeoutException(Exception): + """Exception raised when a timeout during AsyncBehaviour occurs.""" + + +class BaseBehaviourInternalError(Exception): + """Internal error due to a bad implementation of the BaseBehaviour.""" + + def __init__(self, message: str, *args: Any) -> None: + """Initialize the error object.""" + super().__init__("internal error: " + message, *args) + + +class AsyncBehaviour(ABC): + """ + MixIn behaviour class that support limited asynchronous programming. + + An AsyncBehaviour can be in three states: + - READY: no suspended 'async_act' execution; + - RUNNING: 'act' called, and waiting for a message + - WAITING_TICK: 'act' called, and waiting for the next 'act' call + """ + + class AsyncState(Enum): + """Enumeration of AsyncBehaviour states.""" + + READY = "ready" + RUNNING = "running" + WAITING_MESSAGE = "waiting_message" + + def __init__(self) -> None: + """Initialize the async behaviour.""" + self.__state = self.AsyncState.READY + self.__generator_act: Optional[Generator] = None + + # temporary variables for the waiting message state + self.__stopped: bool = True + self.__notified: bool = False + self.__message: Any = None + self.__setup_called: bool = False + + @abstractmethod + def async_act(self) -> Generator: + """Do the act, supporting asynchronous execution.""" + + @abstractmethod + def async_act_wrapper(self) -> Generator: + """Do the act, supporting asynchronous execution.""" + + @property + def state(self) -> AsyncState: + """Get the 'async state'.""" + return self.__state + + @property + def is_notified(self) -> bool: + """Returns whether the behaviour has been notified about the arrival of a message.""" + return self.__notified + + @property + def received_message(self) -> Any: + """Returns the message the behaviour has received. "__message" should be None if not availble or already consumed.""" + return self.__message + + def _on_sent_message(self) -> None: + """To be called after the message received is consumed. Removes the already sent notification and message.""" + self.__notified = False + self.__message = None + + @property + def is_stopped(self) -> bool: + """Check whether the behaviour has stopped.""" + return self.__stopped + + def __get_generator_act(self) -> Generator: + """Get the _generator_act.""" + if self.__generator_act is None: + raise ValueError("generator act not set!") # pragma: nocover + return self.__generator_act + + def try_send(self, message: Any) -> None: + """ + Try to send a message to a waiting behaviour. + + It will be sent only if the behaviour is actually waiting for a message, + and it was not already notified. + + :param message: a Python object. + :raises: SendException if the behaviour was not waiting for a message, + or if it was already notified. + """ + in_waiting_message_state = self.__state == self.AsyncState.WAITING_MESSAGE + already_notified = self.__notified + enforce( + in_waiting_message_state and not already_notified, + "cannot send message", + exception_class=SendException, + ) + self.__notified = True + self.__message = message + + @classmethod + def wait_for_condition( + cls, condition: Callable[[], bool], timeout: Optional[float] = None + ) -> Generator[None, None, None]: + """Wait for a condition to happen. + + This is a local method that does not depend on the global clock, + so the usage of datetime.now() is acceptable here. + + :param condition: the condition to wait for + :param timeout: the maximum amount of time to wait + :yield: None + """ + if timeout is not None: + deadline = datetime.datetime.now() + datetime.timedelta(0, timeout) + else: + deadline = datetime.datetime.max + + while not condition(): + if timeout is not None and datetime.datetime.now() > deadline: + raise TimeoutException() + yield + + def sleep(self, seconds: float) -> Any: + """ + Delay execution for a given number of seconds. + + The argument may be a floating point number for subsecond precision. + This is a local method that does not depend on the global clock, so the + usage of datetime.now() is acceptable here. + + :param seconds: the seconds + :yield: None + """ + deadline = datetime.datetime.now() + datetime.timedelta(0, seconds) + + def _wait_until() -> bool: + return datetime.datetime.now() > deadline + + yield from self.wait_for_condition(_wait_until) + + def wait_for_message( + self, + condition: Callable = lambda message: True, + timeout: Optional[float] = None, + ) -> Any: + """ + Wait for message. + + Care must be taken. This method does not handle concurrent requests. + Use directly after a request is being sent. + This is a local method that does not depend on the global clock, + so the usage of datetime.now() is acceptable here. + + :param condition: a callable + :param timeout: max time to wait (in seconds) + :return: a message + :yield: None + """ + if timeout is not None: + deadline = datetime.datetime.now() + datetime.timedelta(0, timeout) + else: + deadline = datetime.datetime.max + + self.__state = self.AsyncState.WAITING_MESSAGE + try: + message = None + while message is None or not condition(message): + message = yield + if timeout is not None and datetime.datetime.now() > deadline: + raise TimeoutException() + message = cast(Message, message) + return message + finally: + self.__state = self.AsyncState.RUNNING + + def setup(self) -> None: # noqa: B027 # flake8 suggest make it abstract + """Setup behaviour.""" + + def act(self) -> None: + """Do the act.""" + # call setup only the first time act is called + if not self.__setup_called: + self.setup() + self.__setup_called = True + + if self.__state == self.AsyncState.READY: + self.__call_act_first_time() + return + if self.__state == self.AsyncState.WAITING_MESSAGE: + self.__handle_waiting_for_message() + return + enforce(self.__state == self.AsyncState.RUNNING, "not in 'RUNNING' state") + self.__handle_tick() + + def stop(self) -> None: + """Stop the execution of the behaviour.""" + if self.__stopped or self.__state == self.AsyncState.READY: + return + self.__get_generator_act().close() + self.__state = self.AsyncState.READY + self.__stopped = True + + def __call_act_first_time(self) -> None: + """Call the 'async_act' method for the first time.""" + self.__stopped = False + self.__state = self.AsyncState.RUNNING + try: + self.__generator_act = self.async_act_wrapper() + # if the method 'async_act' was not a generator function + # (i.e. no 'yield' or 'yield from' statement) + # just return + if not inspect.isgenerator(self.__generator_act): + self.__state = self.AsyncState.READY + return + # trigger first execution, up to next 'yield' statement + self.__get_generator_act().send(None) + except StopIteration: + # this may happen if the generator is empty + self.__state = self.AsyncState.READY + + def __handle_waiting_for_message(self) -> None: + """Handle an 'act' tick, when waiting for a message.""" + # if there is no message coming, skip. + if self.__notified: + try: + self.__get_generator_act().send(self.__message) + except StopIteration: + self.__handle_stop_iteration() + finally: + # wait for the next message + self.__notified = False + self.__message = None + + def __handle_tick(self) -> None: + """Handle an 'act' tick.""" + try: + self.__get_generator_act().send(None) + except StopIteration: + self.__handle_stop_iteration() + + def __handle_stop_iteration(self) -> None: + """ + Handle 'StopIteration' exception. + + The exception means that the 'async_act' + generator function terminated the execution, + and therefore the state needs to be reset. + """ + self.__state = self.AsyncState.READY + + +class IPFSBehaviour(SimpleBehaviour, ABC): + """Behaviour for interactions with IPFS.""" + + def __init__(self, **kwargs: Any): + """Initialize an `IPFSBehaviour`.""" + super().__init__(**kwargs) + loader_cls = kwargs.pop("loader_cls", Loader) + storer_cls = kwargs.pop("storer_cls", Storer) + self._ipfs_interact = IPFSInteract(loader_cls, storer_cls) + + def _build_ipfs_message( + self, + performative: IpfsMessage.Performative, + timeout: Optional[float] = None, + **kwargs: Any, + ) -> Tuple[IpfsMessage, IpfsDialogue]: + """Builds an IPFS message.""" + ipfs_dialogues = cast(IpfsDialogues, self.context.ipfs_dialogues) + message, dialogue = ipfs_dialogues.create( + counterparty=str(IPFS_CONNECTION_ID), + performative=performative, + timeout=timeout, + **kwargs, + ) + return message, dialogue + + def _build_ipfs_store_file_req( # pylint: disable=too-many-arguments + self, + filename: str, + obj: SupportedObjectType, + multiple: bool = False, + filetype: Optional[SupportedFiletype] = None, + custom_storer: Optional[CustomStorerType] = None, + timeout: Optional[float] = None, + **kwargs: Any, + ) -> Tuple[IpfsMessage, IpfsDialogue]: + """ + Builds a STORE_FILES ipfs message. + + :param filename: the file name to store obj in. If "multiple" is True, filename will be the name of the dir. + :param obj: the object(s) to serialize and store in IPFS as "filename". + :param multiple: whether obj should be stored as multiple files, i.e. directory. + :param custom_storer: a custom serializer for "obj". + :param timeout: timeout for the request. + :returns: the ipfs message, and its corresponding dialogue. + """ + serialized_objects = self._ipfs_interact.store( + filename, obj, multiple, filetype, custom_storer, **kwargs + ) + message, dialogue = self._build_ipfs_message( + performative=IpfsMessage.Performative.STORE_FILES, # type: ignore + files=serialized_objects, + timeout=timeout, + ) + return message, dialogue + + def _build_ipfs_get_file_req( + self, + ipfs_hash: str, + timeout: Optional[float] = None, + ) -> Tuple[IpfsMessage, IpfsDialogue]: + """ + Builds a GET_FILES IPFS request. + + :param ipfs_hash: the ipfs hash of the file/dir to download. + :param timeout: timeout for the request. + :returns: the ipfs message, and its corresponding dialogue. + """ + message, dialogue = self._build_ipfs_message( + performative=IpfsMessage.Performative.GET_FILES, # type: ignore + ipfs_hash=ipfs_hash, + timeout=timeout, + ) + return message, dialogue + + def _deserialize_ipfs_objects( # pylint: disable=too-many-arguments + self, + serialized_objects: Dict[str, str], + filetype: Optional[SupportedFiletype] = None, + custom_loader: CustomLoaderType = None, + ) -> Optional[SupportedObjectType]: + """Deserialize objects received from IPFS.""" + deserialized_object = self._ipfs_interact.load( + serialized_objects, filetype, custom_loader + ) + return deserialized_object + + +class CleanUpBehaviour(SimpleBehaviour, ABC): + """Class for clean-up related functionality of behaviours.""" + + def __init__(self, **kwargs: Any): # pylint: disable=super-init-not-called + """Initialize a base behaviour.""" + SimpleBehaviour.__init__(self, **kwargs) + + def clean_up(self) -> None: + """ + Clean up the resources due to a 'stop' event. + + It can be optionally implemented by the concrete classes. + """ + + def handle_late_messages(self, behaviour_id: str, message: Message) -> None: + """ + Handle late arriving messages. + + Runs from another behaviour, even if the behaviour implementing the method has been exited. + It can be optionally implemented by the concrete classes. + + :param behaviour_id: the id of the behaviour in which the message belongs to. + :param message: the late arriving message to handle. + """ + request_nonce = message.dialogue_reference[0] + self.context.logger.warning( + f"No callback defined for request with nonce: {request_nonce}, arriving for behaviour: {behaviour_id}" + ) + + +class RPCResponseStatus(Enum): + """A custom status of an RPC response.""" + + SUCCESS = 1 + INCORRECT_NONCE = 2 + UNDERPRICED = 3 + INSUFFICIENT_FUNDS = 4 + ALREADY_KNOWN = 5 + UNCLASSIFIED_ERROR = 6 + SIMULATION_FAILED = 7 + + +class _MetaBaseBehaviour(ABCMeta): + """A metaclass that validates BaseBehaviour's attributes.""" + + def __new__(mcs, name: str, bases: Tuple, namespace: Dict, **kwargs: Any) -> Type: # type: ignore + """Initialize the class.""" + new_cls = super().__new__(mcs, name, bases, namespace, **kwargs) + + if ABC in bases: + # abstract class, return + return new_cls + if not issubclass(new_cls, BaseBehaviour): + # the check only applies to AbciApp subclasses + return new_cls + + mcs._check_consistency(cast(Type[BaseBehaviour], new_cls)) + return new_cls + + @classmethod + def _check_consistency(mcs, base_behaviour_cls: Type["BaseBehaviour"]) -> None: + """Check consistency of class attributes.""" + mcs._check_required_class_attributes(base_behaviour_cls) + + @classmethod + def _check_required_class_attributes( + mcs, base_behaviour_cls: Type["BaseBehaviour"] + ) -> None: + """Check that required class attributes are set.""" + if not hasattr(base_behaviour_cls, "matching_round"): + raise BaseBehaviourInternalError( + f"'matching_round' not set on {base_behaviour_cls}" + ) + + +class BaseBehaviour( + AsyncBehaviour, IPFSBehaviour, CleanUpBehaviour, ABC, metaclass=_MetaBaseBehaviour +): + """ + This class represents the base class for FSM behaviours + + A behaviour is a state of the FSM App execution. It usually involves + interactions between participants in the FSM App, + although this is not enforced at this level of abstraction. + + Concrete classes must set: + - matching_round: the round class matching the behaviour; + + Optionally, behaviour_id can be defined, although it is recommended to use the autogenerated id. + """ + + __pattern = re.compile(r"(? str: + """ + Get behaviour id automatically. + + This method returns the auto generated id from the class name if the + class variable behaviour_id is not set on the child class. + Otherwise, it returns the class variable behaviour_id. + """ + return ( + cls.behaviour_id + if isinstance(cls.behaviour_id, str) + else cls.__pattern.sub("_", cls.__name__).lower() + ) + + @property # type: ignore + def behaviour_id(self) -> str: + """Get behaviour id.""" + return self.auto_behaviour_id() + + @property + def params(self) -> BaseParams: + """Return the params.""" + return self.context.params + + @property + def shared_state(self) -> SharedState: + """Return the round sequence.""" + return self.context.state + + @property + def round_sequence(self) -> RoundSequence: + """Return the round sequence.""" + return self.shared_state.round_sequence + + @property + def synchronized_data(self) -> BaseSynchronizedData: + """Return the synchronized data.""" + return self.shared_state.synchronized_data + + @property + def tm_communication_unhealthy(self) -> bool: + """Return if the Tendermint communication is not healthy anymore.""" + return self.round_sequence.block_stall_deadline_expired + + def check_in_round(self, round_id: str) -> bool: + """Check that we entered a specific round.""" + return self.round_sequence.current_round_id == round_id + + def check_in_last_round(self, round_id: str) -> bool: + """Check that we entered a specific round.""" + return self.round_sequence.last_round_id == round_id + + def check_not_in_round(self, round_id: str) -> bool: + """Check that we are not in a specific round.""" + return not self.check_in_round(round_id) + + def check_not_in_last_round(self, round_id: str) -> bool: + """Check that we are not in a specific round.""" + return not self.check_in_last_round(round_id) + + def check_round_has_finished(self, round_id: str) -> bool: + """Check that the round has finished.""" + return self.check_in_last_round(round_id) + + def check_round_height_has_changed(self, round_height: int) -> bool: + """Check that the round height has changed.""" + return self.round_sequence.current_round_height != round_height + + def is_round_ended(self, round_id: str) -> Callable[[], bool]: + """Get a callable to check whether the current round has ended.""" + return partial(self.check_not_in_round, round_id) + + def wait_until_round_end( + self, timeout: Optional[float] = None + ) -> Generator[None, None, None]: + """ + Wait until the ABCI application exits from a round. + + :param timeout: the timeout for the wait + :yield: None + """ + round_id = self.matching_round.auto_round_id() + round_height = self.round_sequence.current_round_height + if self.check_not_in_round(round_id) and self.check_not_in_last_round(round_id): + raise ValueError( + f"Should be in matching round ({round_id}) or last round ({self.round_sequence.last_round_id}), " + f"actual round {self.round_sequence.current_round_id}!" + ) + yield from self.wait_for_condition( + partial(self.check_round_height_has_changed, round_height), timeout=timeout + ) + + def wait_from_last_timestamp(self, seconds: float) -> Any: + """ + Delay execution for a given number of seconds from the last timestamp. + + The argument may be a floating point number for subsecond precision. + This is a local method that does not depend on the global clock, + so the usage of datetime.now() is acceptable here. + + :param seconds: the seconds + :yield: None + """ + if seconds < 0: + raise ValueError("Can only wait for a positive amount of time") + deadline = self.round_sequence.abci_app.last_timestamp + datetime.timedelta( + seconds=seconds + ) + + def _wait_until() -> bool: + return datetime.datetime.now() > deadline + + yield from self.wait_for_condition(_wait_until) + + def is_done(self) -> bool: + """Check whether the behaviour is done.""" + return self._is_done + + def set_done(self) -> None: + """Set the behaviour to done.""" + self._is_done = True + + def send_a2a_transaction( + self, payload: BaseTxPayload, resetting: bool = False + ) -> Generator: + """ + Send transaction and wait for the response, and repeat until not successful. + + :param: payload: the payload to send + :param: resetting: flag indicating if we are resetting Tendermint nodes in this round. + :yield: the responses + """ + stop_condition = self.is_round_ended(self.matching_round.auto_round_id()) + round_count = self.synchronized_data.round_count + object.__setattr__(payload, "round_count", round_count) + yield from self._send_transaction( + payload, + resetting, + stop_condition=stop_condition, + ) + + def async_act_wrapper(self) -> Generator: + """Do the act, supporting asynchronous execution.""" + if not self._is_started: + self._log_start() + self._is_started = True + try: + if self.round_sequence.syncing_up: + yield from self._check_sync() + else: + yield from self.async_act() + except StopIteration: + self.clean_up() + self.set_done() + self._log_end() + return + + if self._is_done: + self._log_end() + + def _check_sync( + self, + ) -> Generator[None, None, None]: + """Check if agent has completed sync.""" + self.context.logger.info("Synchronizing with Tendermint...") + for _ in range(self.context.params.tendermint_max_retries): + self.context.logger.debug( + "Checking status @ " + self.context.params.tendermint_url + "/status", + ) + status = yield from self._get_status() + try: + json_body = json.loads(status.body.decode()) + remote_height = int( + json_body["result"]["sync_info"]["latest_block_height"] + ) + local_height = int(self.round_sequence.height) + _is_sync_complete = local_height == remote_height + if _is_sync_complete: + self.context.logger.info( + f"local height == remote == {local_height}; Synchronization complete." + ) + self.round_sequence.end_sync() + # we set the block stall deadline here because we pinged the /status endpoint + # and received a response from tm, which means that the communication is fine + self.round_sequence.set_block_stall_deadline() + return + yield from self.sleep(self.context.params.tendermint_check_sleep_delay) + except (json.JSONDecodeError, KeyError): # pragma: nocover + self.context.logger.debug( + "Tendermint not accepting transactions yet, trying again!" + ) + yield from self.sleep(self.context.params.tendermint_check_sleep_delay) + + self.context.logger.error("Could not synchronize with Tendermint!") + + def _log_start(self) -> None: + """Log the entering in the behaviour.""" + self.context.logger.info(f"Entered in the '{self.name}' behaviour") + + def _log_end(self) -> None: + """Log the exiting from the behaviour.""" + self.context.logger.info(f"'{self.name}' behaviour is done") + + @classmethod + def _get_request_nonce_from_dialogue(cls, dialogue: Dialogue) -> str: + """Get the request nonce for the request, from the protocol's dialogue.""" + return dialogue.dialogue_label.dialogue_reference[0] + + def _send_transaction( # pylint: disable=too-many-arguments,too-many-locals,too-many-statements + self, + payload: BaseTxPayload, + resetting: bool = False, + stop_condition: Callable[[], bool] = lambda: False, + request_timeout: Optional[float] = None, + request_retry_delay: Optional[float] = None, + tx_timeout: Optional[float] = None, + max_attempts: Optional[int] = None, + ) -> Generator: + """ + Send transaction and wait for the response, repeat until not successful. + + Steps: + - Request the signature of the payload to the Decision Maker + - Send the transaction to the 'price-estimation' app via the Tendermint + node, and wait/repeat until the transaction is not mined. + + Happy-path full flow of the messages. + + get_signature: + AbstractRoundAbci skill -> (SigningMessage | SIGN_MESSAGE) -> DecisionMaker + DecisionMaker -> (SigningMessage | SIGNED_MESSAGE) -> AbstractRoundAbci skill + + _submit_tx: + AbstractRoundAbci skill -> (HttpMessage | REQUEST) -> Http client connection + Http client connection -> (HttpMessage | RESPONSE) -> AbstractRoundAbci skill + + _wait_until_transaction_delivered: + AbstractRoundAbci skill -> (HttpMessage | REQUEST) -> Http client connection + Http client connection -> (HttpMessage | RESPONSE) -> AbstractRoundAbci skill + + :param: payload: the payload to send + :param: resetting: flag indicating if we are resetting Tendermint nodes in this round. + :param: stop_condition: the condition to be checked to interrupt the + waiting loop. + :param: request_timeout: the timeout for the requests + :param: request_retry_delay: the delay to wait after failed requests + :param: tx_timeout: the timeout to wait for tx delivery + :param: max_attempts: max retry attempts + :yield: the responses + """ + request_timeout = ( + self.params.request_timeout if request_timeout is None else request_timeout + ) + request_retry_delay = ( + self.params.request_retry_delay + if request_retry_delay is None + else request_retry_delay + ) + tx_timeout = self.params.tx_timeout if tx_timeout is None else tx_timeout + max_attempts = ( + self.params.max_attempts if max_attempts is None else max_attempts + ) + while not stop_condition(): + self.context.logger.debug( + f"Trying to send payload: {pprint.pformat(payload.json)}" + ) + signature_bytes = yield from self.get_signature(payload.encode()) + transaction = Transaction(payload, signature_bytes) + try: + response = yield from self._submit_tx( + transaction.encode(), timeout=request_timeout + ) + # There is no guarantee that beyond this line will be executed for a given behaviour execution. + # The tx could lead to a round transition which exits us from the behaviour execution. + # It's unlikely to happen anywhere before line 538 but there it is very likely to not + # yield in time before the behaviour is finished. As a result logs below might not show + # up on the happy execution path. + except TimeoutException: + self.context.logger.warning( + f"Timeout expired for submit tx. Retrying in {request_retry_delay} seconds..." + ) + payload = payload.with_new_id() + yield from self.sleep(request_retry_delay) + continue + response = cast(HttpMessage, response) + non_200_code = not self._check_http_return_code_200(response) + if non_200_code and ( + self._non_200_return_code_count + > NON_200_RETURN_CODE_DURING_RESET_THRESHOLD + or not resetting + ): + self.context.logger.error( + f"Received return code != 200 with response {response} with body {str(response.body)}. " + f"Retrying in {request_retry_delay} seconds..." + ) + elif non_200_code and resetting: + self._non_200_return_code_count += 1 + if non_200_code: + payload = payload.with_new_id() + yield from self.sleep(request_retry_delay) + continue + try: + json_body = json.loads(response.body) + except json.JSONDecodeError as e: # pragma: nocover + raise ValueError( + f"Unable to decode response: {response} with body {str(response.body)}" + ) from e + self.context.logger.debug(f"JSON response: {pprint.pformat(json_body)}") + tx_hash = json_body["result"]["hash"] + if json_body["result"]["code"] != OK_CODE: + self.context.logger.error( + f"Received tendermint code != 0. Retrying in {request_retry_delay} seconds..." + ) + yield from self.sleep(request_retry_delay) + continue # pragma: nocover + + try: + is_delivered, res = yield from self._wait_until_transaction_delivered( + tx_hash, + timeout=tx_timeout, + max_attempts=max_attempts, + request_retry_delay=request_retry_delay, + ) + except TimeoutException: + self.context.logger.warning( + f"Timeout expired for wait until transaction delivered. " + f"Retrying in {request_retry_delay} seconds..." + ) + payload = payload.with_new_id() + yield from self.sleep(request_retry_delay) + continue # pragma: nocover + + if is_delivered: + self.context.logger.debug("A2A transaction delivered!") + break + if isinstance(res, HttpMessage) and self._is_invalid_transaction(res): + self.context.logger.error( + f"Tx sent but not delivered. Invalid transaction - not trying again! Response = {res}" + ) + break + # otherwise, repeat until done, or until stop condition is true + if isinstance(res, HttpMessage) and self._tx_not_found(tx_hash, res): + self.context.logger.warning(f"Tx {tx_hash} not found! Response = {res}") + else: + self.context.logger.warning( + f"Tx sent but not delivered. Response = {res}" + ) + payload = payload.with_new_id() + self.context.logger.debug( + "Stop condition is true, no more attempts to send the transaction." + ) + + @staticmethod + def _is_invalid_transaction(res: HttpMessage) -> bool: + """Check if the transaction is invalid.""" + try: + error_codes = ["TransactionNotValidError"] + body_ = json.loads(res.body) + return any( + [error_code in body_["tx_result"]["info"] for error_code in error_codes] + ) + except Exception: # pylint: disable=broad-except # pragma: nocover + return False + + @staticmethod + def _tx_not_found(tx_hash: str, res: HttpMessage) -> bool: + """Check if the transaction could not be found.""" + try: + error = json.loads(res.body)["error"] + not_found_field_to_text = { + "code": -32603, + "message": "Internal error", + "data": f"tx ({tx_hash}) not found", + } + return all( + [ + text == error[field] + for field, text in not_found_field_to_text.items() + ] + ) + except Exception: # pylint: disable=broad-except # pragma: nocover + return False + + def _send_signing_request( + self, raw_message: bytes, is_deprecated_mode: bool = False + ) -> None: + """ + Send a signing request. + + Happy-path full flow of the messages. + + AbstractRoundAbci skill -> (SigningMessage | SIGN_MESSAGE) -> DecisionMaker + DecisionMaker -> (SigningMessage | SIGNED_MESSAGE) -> AbstractRoundAbci skill + + :param raw_message: raw message bytes + :param is_deprecated_mode: is deprecated flag. + """ + signing_dialogues = cast(SigningDialogues, self.context.signing_dialogues) + signing_msg, signing_dialogue = signing_dialogues.create( + counterparty=self.context.decision_maker_address, + performative=SigningMessage.Performative.SIGN_MESSAGE, + raw_message=RawMessage( + self.context.default_ledger_id, + raw_message, + is_deprecated_mode=is_deprecated_mode, + ), + terms=Terms( + ledger_id=self.context.default_ledger_id, + sender_address="", + counterparty_address="", + amount_by_currency_id={}, + quantities_by_good_id={}, + nonce="", + ), + ) + request_nonce = self._get_request_nonce_from_dialogue(signing_dialogue) + cast(Requests, self.context.requests).request_id_to_callback[ + request_nonce + ] = self.get_callback_request() + self.context.decision_maker_message_queue.put_nowait(signing_msg) + + def _send_transaction_signing_request( + self, raw_transaction: RawTransaction, terms: Terms + ) -> None: + """ + Send a transaction signing request. + + Happy-path full flow of the messages. + + AbstractRoundAbci skill -> (SigningMessage | SIGN_TRANSACTION) -> DecisionMaker + DecisionMaker -> (SigningMessage | SIGNED_TRANSACTION) -> AbstractRoundAbci skill + + :param raw_transaction: raw transaction data + :param terms: signing terms + """ + signing_dialogues = cast(SigningDialogues, self.context.signing_dialogues) + signing_msg, signing_dialogue = signing_dialogues.create( + counterparty=self.context.decision_maker_address, + performative=SigningMessage.Performative.SIGN_TRANSACTION, + raw_transaction=raw_transaction, + terms=terms, + ) + request_nonce = self._get_request_nonce_from_dialogue(signing_dialogue) + cast(Requests, self.context.requests).request_id_to_callback[ + request_nonce + ] = self.get_callback_request() + self.context.decision_maker_message_queue.put_nowait(signing_msg) + + def _send_transaction_request( + self, + signing_msg: SigningMessage, + use_flashbots: bool = False, + target_block_numbers: Optional[List[int]] = None, + chain_id: Optional[str] = None, + raise_on_failed_simulation: bool = False, + ) -> None: + """ + Send transaction request. + + Happy-path full flow of the messages. + + AbstractRoundAbci skill -> (LedgerApiMessage | SEND_SIGNED_TRANSACTION) -> Ledger connection + Ledger connection -> (LedgerApiMessage | TRANSACTION_DIGEST) -> AbstractRoundAbci skill + + :param signing_msg: signing message + :param use_flashbots: whether to use flashbots for the transaction or not + :param target_block_numbers: the target block numbers in case we are using flashbots + :param chain_id: the chain name to use for the ledger call + :param raise_on_failed_simulation: whether to raise an exception if the simulation fails or not. + """ + ledger_api_dialogues = cast( + LedgerApiDialogues, self.context.ledger_api_dialogues + ) + + create_kwargs: Dict[ + str, Union[str, SignedTransaction, List[SignedTransaction]] + ] = dict( + counterparty=LEDGER_API_ADDRESS, + performative=LedgerApiMessage.Performative.SEND_SIGNED_TRANSACTION, + ) + if chain_id: + kwargs = LedgerApiMessage.Kwargs({"chain_id": chain_id}) + create_kwargs.update(dict(kwargs=kwargs)) + + if use_flashbots: + _kwargs = { + "chain_id": chain_id, + "raise_on_failed_simulation": raise_on_failed_simulation, + "use_all_builders": True, # TODO: make this a proper parameter + } + if target_block_numbers is not None: + _kwargs["target_block_numbers"] = target_block_numbers # type: ignore + create_kwargs.update( + dict( + performative=LedgerApiMessage.Performative.SEND_SIGNED_TRANSACTIONS, + # we do not support sending multiple signed txs and receiving multiple tx hashes yet + signed_transactions=LedgerApiMessage.SignedTransactions( + ledger_id=FLASHBOTS_LEDGER_ID, + signed_transactions=[signing_msg.signed_transaction.body], + ), + kwargs=LedgerApiMessage.Kwargs(_kwargs), + ) + ) + else: + create_kwargs.update( + dict( + signed_transaction=signing_msg.signed_transaction, + ) + ) + + ledger_api_msg, ledger_api_dialogue = ledger_api_dialogues.create( + **create_kwargs + ) + ledger_api_dialogue = cast(LedgerApiDialogue, ledger_api_dialogue) + request_nonce = self._get_request_nonce_from_dialogue(ledger_api_dialogue) + cast(Requests, self.context.requests).request_id_to_callback[ + request_nonce + ] = self.get_callback_request() + self.context.outbox.put_message(message=ledger_api_msg) + self.context.logger.debug("Sending transaction to ledger...") + + def _send_transaction_receipt_request( + self, + tx_digest: str, + retry_timeout: Optional[int] = None, + retry_attempts: Optional[int] = None, + **kwargs: Any, + ) -> None: + """ + Send transaction receipt request. + + Happy-path full flow of the messages. + + AbstractRoundAbci skill -> (LedgerApiMessage | GET_TRANSACTION_RECEIPT) -> Ledger connection + Ledger connection -> (LedgerApiMessage | TRANSACTION_RECEIPT) -> AbstractRoundAbci skill + + :param tx_digest: transaction digest string + :param retry_timeout: retry timeout in seconds + :param retry_attempts: number of retry attempts + """ + ledger_api_dialogues = cast( + LedgerApiDialogues, self.context.ledger_api_dialogues + ) + ledger_api_msg, ledger_api_dialogue = ledger_api_dialogues.create( + counterparty=LEDGER_API_ADDRESS, + performative=LedgerApiMessage.Performative.GET_TRANSACTION_RECEIPT, + transaction_digest=LedgerApiMessage.TransactionDigest( + ledger_id=self.context.default_ledger_id, body=tx_digest + ), + retry_timeout=retry_timeout, + retry_attempts=retry_attempts, + kwargs=LedgerApiMessage.Kwargs(kwargs), + ) + ledger_api_dialogue = cast(LedgerApiDialogue, ledger_api_dialogue) + request_nonce = self._get_request_nonce_from_dialogue(ledger_api_dialogue) + cast(Requests, self.context.requests).request_id_to_callback[ + request_nonce + ] = self.get_callback_request() + self.context.outbox.put_message(message=ledger_api_msg) + self.context.logger.debug( + f"Sending transaction receipt request for tx_digest='{tx_digest}'..." + ) + + def _handle_signing_failure(self) -> None: + """Handle signing failure.""" + self.context.logger.error("The transaction could not be signed!") + + def _submit_tx( + self, tx_bytes: bytes, timeout: Optional[float] = None + ) -> Generator[None, None, HttpMessage]: + """Send a broadcast_tx_sync request. + + Happy-path full flow of the messages. + + _do_request: + AbstractRoundAbci skill -> (HttpMessage | REQUEST) -> Http client connection + Http client connection -> (HttpMessage | RESPONSE) -> AbstractRoundAbci skill + + :param tx_bytes: transaction bytes + :param timeout: timeout seconds + :yield: HttpMessage object + :return: http response + """ + request_message, http_dialogue = self._build_http_request_message( + "GET", + self.context.params.tendermint_url + + f"/broadcast_tx_sync?tx=0x{tx_bytes.hex()}", + ) + result = yield from self._do_request( + request_message, http_dialogue, timeout=timeout + ) + return result + + def _get_tx_info( + self, tx_hash: str, timeout: Optional[float] = None + ) -> Generator[None, None, HttpMessage]: + """ + Get transaction info from tx hash. + + Happy-path full flow of the messages. + + _do_request: + AbstractRoundAbci skill -> (HttpMessage | REQUEST) -> Http client connection + Http client connection -> (HttpMessage | RESPONSE) -> AbstractRoundAbci skill + + :param tx_hash: transaction hash + :param timeout: timeout in seconds + :yield: HttpMessage object + :return: http response + """ + request_message, http_dialogue = self._build_http_request_message( + "GET", + self.context.params.tendermint_url + f"/tx?hash=0x{tx_hash}", + ) + result = yield from self._do_request( + request_message, http_dialogue, timeout=timeout + ) + return result + + def _get_status(self) -> Generator[None, None, HttpMessage]: + """ + Get Tendermint node's status. + + Happy-path full flow of the messages. + + _do_request: + AbstractRoundAbci skill -> (HttpMessage | REQUEST) -> Http client connection + Http client connection -> (HttpMessage | RESPONSE) -> AbstractRoundAbci skill + + :yield: HttpMessage object + :return: http response from tendermint + """ + request_message, http_dialogue = self._build_http_request_message( + "GET", + self.context.params.tendermint_url + "/status", + ) + result = yield from self._do_request(request_message, http_dialogue) + return result + + def _get_netinfo( + self, timeout: Optional[float] = None + ) -> Generator[None, None, HttpMessage]: + """Makes a GET request to it's tendermint node's /net_info endpoint.""" + request_message, http_dialogue = self._build_http_request_message( + method="GET", url=f"{self.context.params.tendermint_url}/net_info" + ) + result = yield from self._do_request(request_message, http_dialogue, timeout) + return result + + def num_active_peers( + self, timeout: Optional[float] = None + ) -> Generator[None, None, Optional[int]]: + """Returns the number of active peers in the network.""" + try: + http_response = yield from self._get_netinfo(timeout) + http_ok = 200 + if http_response.status_code != http_ok: + # a bad response was received, we cannot retrieve the number of active peers + self.context.logger.warning( + f"`/net_info` responded with status {http_response.status_code}." + ) + return None + + res_body = json.loads(http_response.body) + num_peers_str = res_body.get("result", {}).get("n_peers", None) + if num_peers_str is None: + return None + num_peers = int(num_peers_str) + # num_peers hold the number of peers the tm node we are + # making the TX to currently has an active connection + # we add 1 because the node we are making the request through + # is not accounted for in this number + return num_peers + 1 + except TimeoutException: + self.context.logger.warning( + f"Couldn't retrieve `/net_info` response in {timeout}s." + ) + return None + + def get_callback_request(self) -> Callable[[Message, "BaseBehaviour"], None]: + """Wrapper for callback request which depends on whether the message has not been handled on time. + + :return: the request callback. + """ + + def callback_request( + message: Message, current_behaviour: BaseBehaviour + ) -> None: + """The callback request.""" + if self.is_stopped: + self.context.logger.debug( + "Dropping message as behaviour has stopped: %s", message + ) + elif self != current_behaviour: + self.handle_late_messages(self.behaviour_id, message) + elif self.state == AsyncBehaviour.AsyncState.WAITING_MESSAGE: + self.try_send(message) + else: + self.context.logger.warning( + "Could not send message to FSMBehaviour: %s", message + ) + + return callback_request + + def get_http_response( + self, + method: str, + url: str, + content: Optional[bytes] = None, + headers: Optional[Dict[str, str]] = None, + parameters: Optional[Dict[str, str]] = None, + ) -> Generator[None, None, HttpMessage]: + """ + Send an http request message from the skill context. + + This method is skill-specific, and therefore + should not be used elsewhere. + + Happy-path full flow of the messages. + + _do_request: + AbstractRoundAbci skill -> (HttpMessage | REQUEST) -> Http client connection + Http client connection -> (HttpMessage | RESPONSE) -> AbstractRoundAbci skill + + :param method: the http request method (i.e. 'GET' or 'POST'). + :param url: the url to send the message to. + :param content: the payload. + :param headers: headers to be included. + :param parameters: url query parameters. + :yield: HttpMessage object + :return: the http message and the http dialogue + """ + http_message, http_dialogue = self._build_http_request_message( + method=method, + url=url, + content=content, + headers=headers, + parameters=parameters, + ) + response = yield from self._do_request(http_message, http_dialogue) + return response + + def _do_request( + self, + request_message: HttpMessage, + http_dialogue: HttpDialogue, + timeout: Optional[float] = None, + ) -> Generator[None, None, HttpMessage]: + """ + Do a request and wait the response, asynchronously. + + Happy-path full flow of the messages. + + AbstractRoundAbci skill -> (HttpMessage | REQUEST) -> Http client connection + Http client connection -> (HttpMessage | RESPONSE) -> AbstractRoundAbci skill + + :param request_message: The request message + :param http_dialogue: the HTTP dialogue associated to the request + :param timeout: seconds to wait for the reply. + :yield: HttpMessage object + :return: the response message + """ + self.context.outbox.put_message(message=request_message) + request_nonce = self._get_request_nonce_from_dialogue(http_dialogue) + cast(Requests, self.context.requests).request_id_to_callback[ + request_nonce + ] = self.get_callback_request() + # notify caller by propagating potential timeout exception. + response = yield from self.wait_for_message(timeout=timeout) + return response + + def _build_http_request_message( + self, + method: str, + url: str, + content: Optional[bytes] = None, + headers: Optional[Dict[str, str]] = None, + parameters: Optional[Dict[str, str]] = None, + ) -> Tuple[HttpMessage, HttpDialogue]: + """ + Send an http request message from the skill context. + + This method is skill-specific, and therefore + should not be used elsewhere. + + :param method: the http request method (i.e. 'GET' or 'POST'). + :param url: the url to send the message to. + :param content: the payload. + :param headers: headers to be included. + :param parameters: url query parameters. + :return: the http message and the http dialogue + """ + if parameters: + url = url + "?" + for key, val in parameters.items(): + url += f"{key}={val}&" + url = url[:-1] + + header_string = "" + if headers: + for key, val in headers.items(): + header_string += f"{key}: {val}\r\n" + + # context + http_dialogues = cast(HttpDialogues, self.context.http_dialogues) + + # http request message + request_http_message, http_dialogue = http_dialogues.create( + counterparty=str(HTTP_CLIENT_PUBLIC_ID), + performative=HttpMessage.Performative.REQUEST, + method=method, + url=url, + headers=header_string, + version="", + body=b"" if content is None else content, + ) + request_http_message = cast(HttpMessage, request_http_message) + http_dialogue = cast(HttpDialogue, http_dialogue) + return request_http_message, http_dialogue + + def _wait_until_transaction_delivered( + self, + tx_hash: str, + timeout: Optional[float] = None, + max_attempts: Optional[int] = None, + request_retry_delay: Optional[float] = None, + ) -> Generator[None, None, Tuple[bool, Optional[HttpMessage]]]: + """ + Wait until transaction is delivered. + + Happy-path full flow of the messages. + + _get_tx_info: + AbstractRoundAbci skill -> (HttpMessage | REQUEST) -> Http client connection + Http client connection -> (HttpMessage | RESPONSE) -> AbstractRoundAbci skill + + This is a local method that does not depend on the global clock, + so the usage of datetime.now() is acceptable here. + + :param tx_hash: the transaction hash to check. + :param timeout: timeout + :param: request_retry_delay: the delay to wait after failed requests + :param: max_attempts: the maximun number of attempts + :yield: None + :return: True if it is delivered successfully, False otherwise + """ + if timeout is not None: + deadline = datetime.datetime.now() + datetime.timedelta(0, timeout) + else: + deadline = datetime.datetime.max + request_retry_delay = ( + self.params.request_retry_delay + if request_retry_delay is None + else request_retry_delay + ) + max_attempts = ( + self.params.max_attempts if max_attempts is None else max_attempts + ) + + response = None + for _ in range(max_attempts): + request_timeout = ( + (deadline - datetime.datetime.now()).total_seconds() + if timeout is not None + else None + ) + if request_timeout is not None and request_timeout < 0: + raise TimeoutException() + + response = yield from self._get_tx_info(tx_hash, timeout=request_timeout) + if response.status_code != 200: + yield from self.sleep(request_retry_delay) + continue + + try: + json_body = json.loads(response.body) + except json.JSONDecodeError as e: # pragma: nocover + raise ValueError( + f"Unable to decode response: {response} with body {str(response.body)}" + ) from e + tx_result = json_body["result"]["tx_result"] + return tx_result["code"] == OK_CODE, response + + return False, response + + @classmethod + def _check_http_return_code_200(cls, response: HttpMessage) -> bool: + """Check the HTTP response has return code 200.""" + return response.status_code == 200 + + def _get_default_terms(self) -> Terms: + """ + Get default transaction terms. + + :return: terms + """ + terms = Terms( + ledger_id=self.context.default_ledger_id, + sender_address=self.context.agent_address, + counterparty_address=self.context.agent_address, + amount_by_currency_id={}, + quantities_by_good_id={}, + nonce="", + ) + return terms + + def get_signature( + self, message: bytes, is_deprecated_mode: bool = False + ) -> Generator[None, None, str]: + """ + Get signature for message. + + Happy-path full flow of the messages. + + _send_signing_request: + AbstractRoundAbci skill -> (SigningMessage | SIGN_MESSAGE) -> DecisionMaker + DecisionMaker -> (SigningMessage | SIGNED_MESSAGE) -> AbstractRoundAbci skill + + :param message: message bytes + :param is_deprecated_mode: is deprecated mode flag + :yield: SigningMessage object + :return: message signature + """ + self._send_signing_request(message, is_deprecated_mode) + signature_response = yield from self.wait_for_message() + signature_response = cast(SigningMessage, signature_response) + if signature_response.performative == SigningMessage.Performative.ERROR: + self._handle_signing_failure() + raise RuntimeError("Internal error: failure during signing.") + signature_bytes = signature_response.signed_message.body + return signature_bytes + + def send_raw_transaction( + self, + transaction: RawTransaction, + use_flashbots: bool = False, + target_block_numbers: Optional[List[int]] = None, + raise_on_failed_simulation: bool = False, + chain_id: Optional[str] = None, + ) -> Generator[ + None, + Union[None, SigningMessage, LedgerApiMessage], + Tuple[Optional[str], RPCResponseStatus], + ]: + """ + Send raw transactions to the ledger for mining. + + Happy-path full flow of the messages. + + _send_transaction_signing_request: + AbstractRoundAbci skill -> (SigningMessage | SIGN_TRANSACTION) -> DecisionMaker + DecisionMaker -> (SigningMessage | SIGNED_TRANSACTION) -> AbstractRoundAbci skill + + _send_transaction_request: + AbstractRoundAbci skill -> (LedgerApiMessage | SEND_SIGNED_TRANSACTION) -> Ledger connection + Ledger connection -> (LedgerApiMessage | TRANSACTION_DIGEST) -> AbstractRoundAbci skill + + :param transaction: transaction data + :param use_flashbots: whether to use flashbots for the transaction or not + :param target_block_numbers: the target block numbers in case we are using flashbots + :param raise_on_failed_simulation: whether to raise an exception if the transaction fails the simulation or not + :param chain_id: the chain name to use for the ledger call + :yield: SigningMessage object + :return: transaction hash + """ + if chain_id is None: + chain_id = self.params.default_chain_id + + terms = Terms( + chain_id, + self.context.agent_address, + counterparty_address="", + amount_by_currency_id={}, + quantities_by_good_id={}, + nonce="", + ) + self.context.logger.info( + f"Sending signing request to ledger '{chain_id}' for transaction: {transaction}..." + ) + self._send_transaction_signing_request(transaction, terms) + signature_response = yield from self.wait_for_message() + signature_response = cast(SigningMessage, signature_response) + tx_hash_backup = signature_response.signed_transaction.body.get("hash") + if ( + signature_response.performative + != SigningMessage.Performative.SIGNED_TRANSACTION + ): + self.context.logger.error( + f"Error when requesting transaction signature: {signature_response}" + ) + return None, RPCResponseStatus.UNCLASSIFIED_ERROR + self.context.logger.info( + f"Received signature response: {signature_response}\n Sending transaction..." + ) + self._send_transaction_request( + signature_response, + use_flashbots, + target_block_numbers, + chain_id, + raise_on_failed_simulation, + ) + transaction_digest_msg = yield from self.wait_for_message() + transaction_digest_msg = cast(LedgerApiMessage, transaction_digest_msg) + performative = transaction_digest_msg.performative + if performative not in ( + LedgerApiMessage.Performative.TRANSACTION_DIGEST, + LedgerApiMessage.Performative.TRANSACTION_DIGESTS, + ): + error = f"Error when requesting transaction digest: {transaction_digest_msg.message}" + self.context.logger.error(error) + return tx_hash_backup, self.__parse_rpc_error(error) + + tx_hash = ( + # we do not support sending multiple messages and receiving multiple tx hashes yet + transaction_digest_msg.transaction_digests.transaction_digests[0] + if performative == LedgerApiMessage.Performative.TRANSACTION_DIGESTS + else transaction_digest_msg.transaction_digest.body + ) + + self.context.logger.info( + f"Transaction sent! Received transaction digest: {tx_hash}" + ) + + if tx_hash != tx_hash_backup: + # this should never happen + self.context.logger.error( + f"Unexpected error! The signature response's hash `{tx_hash_backup}` " + f"does not match the one received from the transaction response `{tx_hash}`!" + ) + return None, RPCResponseStatus.UNCLASSIFIED_ERROR + + return tx_hash, RPCResponseStatus.SUCCESS + + def get_transaction_receipt( + self, + tx_digest: str, + retry_timeout: Optional[int] = None, + retry_attempts: Optional[int] = None, + chain_id: Optional[str] = None, + ) -> Generator[None, None, Optional[Dict]]: + """ + Get transaction receipt. + + Happy-path full flow of the messages. + + _send_transaction_receipt_request: + AbstractRoundAbci skill -> (LedgerApiMessage | GET_TRANSACTION_RECEIPT) -> Ledger connection + Ledger connection -> (LedgerApiMessage | TRANSACTION_RECEIPT) -> AbstractRoundAbci skill + + :param tx_digest: transaction digest received from raw transaction. + :param retry_timeout: retry timeout. + :param retry_attempts: number of retry attempts allowed. + :yield: LedgerApiMessage object + :return: transaction receipt data + """ + if chain_id is None: + chain_id = self.params.default_chain_id + self._send_transaction_receipt_request( + tx_digest, retry_timeout, retry_attempts, chain_id=chain_id + ) + transaction_receipt_msg = yield from self.wait_for_message() + if ( + transaction_receipt_msg.performative == LedgerApiMessage.Performative.ERROR + ): # pragma: nocover + self.context.logger.error( + f"Error when requesting transaction receipt: {transaction_receipt_msg.message}" + ) + return None + tx_receipt = transaction_receipt_msg.transaction_receipt.receipt + return tx_receipt + + def get_ledger_api_response( + self, + performative: LedgerApiMessage.Performative, + ledger_callable: str, + **kwargs: Any, + ) -> Generator[None, None, LedgerApiMessage]: + """ + Request data from ledger api + + Happy-path full flow of the messages. + + AbstractRoundAbci skill -> (LedgerApiMessage | LedgerApiMessage.Performative) -> Ledger connection + Ledger connection -> (LedgerApiMessage | LedgerApiMessage.Performative) -> AbstractRoundAbci skill + + :param performative: the message performative + :param ledger_callable: the callable to call on the contract + :param kwargs: keyword argument for the contract api request + :return: the contract api response + :yields: the contract api response + """ + ledger_api_dialogues = cast( + LedgerApiDialogues, self.context.ledger_api_dialogues + ) + kwargs = { + "performative": performative, + "counterparty": LEDGER_API_ADDRESS, + "ledger_id": self.context.default_ledger_id, + "callable": ledger_callable, + "kwargs": LedgerApiMessage.Kwargs(kwargs), + "args": tuple(), + } + ledger_api_msg, ledger_api_dialogue = ledger_api_dialogues.create(**kwargs) + ledger_api_dialogue = cast( + LedgerApiDialogue, + ledger_api_dialogue, + ) + ledger_api_dialogue.terms = self._get_default_terms() + request_nonce = self._get_request_nonce_from_dialogue(ledger_api_dialogue) + cast(Requests, self.context.requests).request_id_to_callback[ + request_nonce + ] = self.get_callback_request() + self.context.outbox.put_message(message=ledger_api_msg) + response = yield from self.wait_for_message() + return response + + def get_contract_api_response( + self, + performative: ContractApiMessage.Performative, + contract_address: Optional[str], + contract_id: str, + contract_callable: str, + ledger_id: Optional[str] = None, + **kwargs: Any, + ) -> Generator[None, None, ContractApiMessage]: + """ + Request contract safe transaction hash + + Happy-path full flow of the messages. + + AbstractRoundAbci skill -> (ContractApiMessage | ContractApiMessage.Performative) -> Ledger connection (contract dispatcher) + Ledger connection (contract dispatcher) -> (ContractApiMessage | ContractApiMessage.Performative) -> AbstractRoundAbci skill + + :param performative: the message performative + :param contract_address: the contract address + :param contract_id: the contract id + :param contract_callable: the callable to call on the contract + :param ledger_id: the ledger id, if not specified, the default ledger id is used + :param kwargs: keyword argument for the contract api request + :return: the contract api response + :yields: the contract api response + """ + contract_api_dialogues = cast( + ContractApiDialogues, self.context.contract_api_dialogues + ) + kwargs = { + "performative": performative, + "counterparty": LEDGER_API_ADDRESS, + "ledger_id": ledger_id or self.context.default_ledger_id, + "contract_id": contract_id, + "callable": contract_callable, + "kwargs": ContractApiMessage.Kwargs(kwargs), + } + if contract_address is not None: + kwargs["contract_address"] = contract_address + contract_api_msg, contract_api_dialogue = contract_api_dialogues.create( + **kwargs + ) + contract_api_dialogue = cast( + ContractApiDialogue, + contract_api_dialogue, + ) + contract_api_dialogue.terms = self._get_default_terms() + request_nonce = self._get_request_nonce_from_dialogue(contract_api_dialogue) + cast(Requests, self.context.requests).request_id_to_callback[ + request_nonce + ] = self.get_callback_request() + self.context.outbox.put_message(message=contract_api_msg) + response = yield from self.wait_for_message() + return response + + @staticmethod + def __parse_rpc_error(error: str) -> RPCResponseStatus: + """Parse an RPC error and return an `RPCResponseStatus`""" + if "replacement transaction underpriced" in error: + return RPCResponseStatus.UNDERPRICED + if "nonce too low" in error: + return RPCResponseStatus.INCORRECT_NONCE + if "insufficient funds" in error: + return RPCResponseStatus.INSUFFICIENT_FUNDS + if "already known" in error: + return RPCResponseStatus.ALREADY_KNOWN + if "Simulation failed for bundle" in error: + return RPCResponseStatus.SIMULATION_FAILED + return RPCResponseStatus.UNCLASSIFIED_ERROR + + def _acn_request_from_pending( + self, performative: TendermintMessage.Performative + ) -> Generator: + """Perform an ACN request to each one of the agents which have not sent a response yet.""" + not_responded_yet = { + address + for address, deliverable in self.shared_state.address_to_acn_deliverable.items() + if deliverable is None + } + + if len(not_responded_yet) == 0: + return + + self.context.logger.debug(f"Need ACN response from {not_responded_yet}.") + for address in not_responded_yet: + self.context.logger.debug(f"Sending ACN request to {address}.") + dialogues = cast(TendermintDialogues, self.context.tendermint_dialogues) + message, _ = dialogues.create( + counterparty=address, performative=performative + ) + message = cast(TendermintMessage, message) + context = EnvelopeContext(connection_id=P2P_LIBP2P_CLIENT_PUBLIC_ID) + self.context.outbox.put_message(message=message, context=context) + + # we wait for the `address_to_acn_deliverable` to be populated with the responses (done by the tm handler) + yield from self.sleep(self.params.sleep_time) + + def _perform_acn_request( + self, performative: TendermintMessage.Performative + ) -> Generator[None, None, Any]: + """Perform an ACN request. + + Waits `sleep_time` to receive a common response from the majority of the agents. + Retries `max_attempts` times only for the agents which have not responded yet. + + :param performative: the ACN request performative. + :return: the result that the majority of the agents sent. If majority cannot be reached, returns `None`. + """ + # reset the ACN deliverables at the beginning of a new request + self.shared_state.address_to_acn_deliverable = self.shared_state.acn_container() + + result = None + for i in range(self.params.max_attempts): + self.context.logger.debug( + f"ACN attempt {i + 1}/{self.params.max_attempts}." + ) + yield from self._acn_request_from_pending(performative) + + result = self.shared_state.get_acn_result() + if result is not None: + break + + return result + + def request_recovery_params(self, should_log: bool) -> Generator[None, None, bool]: + """Request the Tendermint recovery parameters from the other agents via the ACN.""" + + if should_log: + self.context.logger.info( + "Requesting the Tendermint recovery parameters from the other agents via the ACN..." + ) + + performative = TendermintMessage.Performative.GET_RECOVERY_PARAMS + acn_result = yield from self._perform_acn_request(performative) # type: ignore + + if acn_result is None: + if should_log: + self.context.logger.warning( + "No majority has been reached for the Tendermint recovery parameters request via the ACN." + ) + return False + + self.shared_state.tm_recovery_params = acn_result + if should_log: + self.context.logger.info( + f"Updated the Tendermint recovery parameters from the other agents via the ACN: {acn_result}" + ) + return True + + @property + def hard_reset_sleep(self) -> float: + """ + Amount of time to sleep before and after performing a hard reset. + + We sleep for half the reset pause duration as there are no immediate transactions on either side of the reset. + + :returns: the amount of time to sleep in seconds + """ + return self.params.reset_pause_duration / 2 + + def _start_reset(self, on_startup: bool = False) -> Generator: + """ + Start tendermint reset. + + This is a local method that does not depend on the global clock, + so the usage of datetime.now() is acceptable here. + + :param on_startup: Whether we are resetting on the start of the agent. + :yield: None + """ + if self._check_started is None and not self._is_healthy: + if not on_startup: + # if we are on startup we don't need to wait for the reset pause duration + # as the reset is being performed to update the tm config. + yield from self.wait_from_last_timestamp(self.hard_reset_sleep) + self._check_started = datetime.datetime.now() + self._timeout = self.params.max_healthcheck + self._is_healthy = False + yield + + def _end_reset( + self, + ) -> None: + """End tendermint reset. + + This is a local method that does not depend on the global clock, + so the usage of datetime.now() is acceptable here. + """ + self._check_started = None + self._timeout = -1.0 + self._is_healthy = True + + def _is_timeout_expired(self) -> bool: + """Check if the timeout expired. + + This is a local method that does not depend on the global clock, + so the usage of datetime.now() is acceptable here. + + :return: bool + """ + if self._check_started is None or self._is_healthy: + return False + return datetime.datetime.now() > self._check_started + datetime.timedelta( + 0, self._timeout + ) + + def _get_reset_params(self, default: bool) -> Optional[Dict[str, str]]: + """Get the parameters for a hard reset request to Tendermint.""" + if default: + return None + + last_round_transition_timestamp = ( + self.round_sequence.last_round_transition_timestamp + ) + genesis_time = last_round_transition_timestamp.astimezone(pytz.UTC).strftime( + GENESIS_TIME_FMT + ) + return { + "genesis_time": genesis_time, + "initial_height": INITIAL_HEIGHT, + "period_count": str(self.synchronized_data.period_count), + } + + def reset_tendermint_with_wait( # pylint: disable=too-many-locals, too-many-statements + self, + on_startup: bool = False, + is_recovery: bool = False, + ) -> Generator[None, None, bool]: + """ + Performs a hard reset (unsafe-reset-all) on the tendermint node. + + :param on_startup: whether we are resetting on the start of the agent. + :param is_recovery: whether the reset is being performed to recover the agent <-> tm communication. + :yields: None + :returns: whether the reset was successful. + """ + yield from self._start_reset(on_startup=on_startup) + if self._is_timeout_expired(): + # if the Tendermint node cannot update the app then the app cannot work + raise RuntimeError("Error resetting tendermint node.") + + if not self._is_healthy: + self.context.logger.info( + f"Resetting tendermint node at end of period={self.synchronized_data.period_count}." + ) + + backup_blockchain = self.round_sequence.blockchain + self.round_sequence.reset_blockchain() + reset_params = self._get_reset_params(on_startup) + request_message, http_dialogue = self._build_http_request_message( + "GET", + self.params.tendermint_com_url + "/hard_reset", + parameters=reset_params, + ) + result = yield from self._do_request(request_message, http_dialogue) + try: + response = json.loads(result.body.decode()) + if response.get("status"): + self.context.logger.debug(response.get("message")) + self.context.logger.info("Resetting tendermint node successful!") + is_replay = response.get("is_replay", False) + if is_replay: + # in case of replay, the local blockchain should be set up differently. + self.round_sequence.reset_blockchain( + is_replay=is_replay, is_init=True + ) + for handler_name in self.context.handlers.__dict__.keys(): + dialogues = getattr(self.context, f"{handler_name}_dialogues") + dialogues.cleanup() + if not is_recovery: + # in case of successful reset we store the reset params in the shared state, + # so that in the future if the communication with tendermint breaks, and we need to + # perform a hard reset to restore it, we can use these as the right ones + round_count = self.synchronized_data.db.round_count - 1 + # in case we need to reset in order to recover agent <-> tm communication + # we store this round as the one to start from + restart_from_round = self.matching_round + self.shared_state.tm_recovery_params = TendermintRecoveryParams( + reset_params=reset_params, + round_count=round_count, + reset_from_round=restart_from_round.auto_round_id(), + serialized_db_state=self.shared_state.synchronized_data.db.serialize(), + ) + self.round_sequence.abci_app.cleanup( + self.params.cleanup_history_depth, + self.params.cleanup_history_depth_current, + ) + self._end_reset() + + else: + msg = response.get("message") + self.round_sequence.blockchain = backup_blockchain + self.context.logger.error(f"Error resetting: {msg}") + yield from self.sleep(self.params.sleep_time) + return False + except json.JSONDecodeError: + self.context.logger.error( + "Error communicating with tendermint com server." + ) + self.round_sequence.blockchain = backup_blockchain + yield from self.sleep(self.params.sleep_time) + return False + + status = yield from self._get_status() + try: + json_body = json.loads(status.body.decode()) + except json.JSONDecodeError: + self.context.logger.error( + "Tendermint not accepting transactions yet, trying again!" + ) + yield from self.sleep(self.params.sleep_time) + return False + + remote_height = int(json_body["result"]["sync_info"]["latest_block_height"]) + local_height = self.round_sequence.height + if local_height != remote_height: + self.context.logger.warning( + f"local height ({local_height}) != remote height ({remote_height}); retrying..." + ) + yield from self.sleep(self.params.sleep_time) + return False + + self.context.logger.info( + f"local height == remote height == {local_height}; continuing execution..." + ) + if not on_startup: + # if we are on startup we don't need to wait for the reset pause duration + # as the reset is being performed to update the tm config. + yield from self.wait_from_last_timestamp(self.hard_reset_sleep) + self._is_healthy = False + return True + + def send_to_ipfs( # pylint: disable=too-many-arguments + self, + filename: str, + obj: SupportedObjectType, + multiple: bool = False, + filetype: Optional[SupportedFiletype] = None, + custom_storer: Optional[CustomStorerType] = None, + timeout: Optional[float] = None, + **kwargs: Any, + ) -> Generator[None, None, Optional[str]]: + """ + Store an object on IPFS. + + :param filename: the file name to store obj in. If "multiple" is True, filename will be the name of the dir. + :param obj: the object(s) to serialize and store in IPFS as "filename". + :param multiple: whether obj should be stored as multiple files, i.e. directory. + :param filetype: the file type of the object being downloaded. + :param custom_storer: a custom serializer for "obj". + :param timeout: timeout for the request. + :returns: the downloaded object, corresponding to ipfs_hash. + """ + try: + message, dialogue = self._build_ipfs_store_file_req( + filename, + obj, + multiple, + filetype, + custom_storer, + timeout, + **kwargs, + ) + ipfs_message = yield from self._do_ipfs_request(dialogue, message, timeout) + if ipfs_message.performative != IpfsMessage.Performative.IPFS_HASH: + self.context.logger.error( + f"Expected performative {IpfsMessage.Performative.IPFS_HASH} but got {ipfs_message.performative}." + ) + return None + ipfs_hash = ipfs_message.ipfs_hash + self.context.logger.info( + f"Successfully stored {filename} to IPFS with hash: {ipfs_hash}" + ) + return ipfs_hash + except IPFSInteractionError as e: # pragma: no cover + self.context.logger.error( + f"An error occurred while trying to send a file to IPFS: {str(e)}" + ) + return None + + def get_from_ipfs( # pylint: disable=too-many-arguments + self, + ipfs_hash: str, + filetype: Optional[SupportedFiletype] = None, + custom_loader: CustomLoaderType = None, + timeout: Optional[float] = None, + ) -> Generator[None, None, Optional[SupportedObjectType]]: + """ + Gets an object from IPFS. + + :param ipfs_hash: the ipfs hash of the file/dir to download. + :param filetype: the file type of the object being downloaded. + :param custom_loader: a custom deserializer for the object received from IPFS. + :param timeout: timeout for the request. + :returns: the downloaded object, corresponding to ipfs_hash. + """ + try: + message, dialogue = self._build_ipfs_get_file_req(ipfs_hash, timeout) + ipfs_message = yield from self._do_ipfs_request(dialogue, message, timeout) + if ipfs_message.performative != IpfsMessage.Performative.FILES: + self.context.logger.error( + f"Expected performative {IpfsMessage.Performative.FILES} but got {ipfs_message.performative}." + ) + return None + serialized_objects = ipfs_message.files + deserialized_objects = self._deserialize_ipfs_objects( + serialized_objects, filetype, custom_loader + ) + self.context.logger.info( + f"Retrieved {len(ipfs_message.files)} objects from ipfs." + ) + return deserialized_objects + except IPFSInteractionError as e: + self.context.logger.error( + f"An error occurred while trying to fetch a file from IPFS: {str(e)}" + ) + return None + + def _do_ipfs_request( + self, + dialogue: IpfsDialogue, + message: IpfsMessage, + timeout: Optional[float] = None, + ) -> Generator[None, None, IpfsMessage]: + """Performs an IPFS request, and asynchronosuly waits for response.""" + self.context.outbox.put_message(message=message) + request_nonce = self._get_request_nonce_from_dialogue(dialogue) + cast(Requests, self.context.requests).request_id_to_callback[ + request_nonce + ] = self.get_callback_request() + # notify caller by propagating potential timeout exception. + response = yield from self.wait_for_message(timeout=timeout) + ipfs_message = cast(IpfsMessage, response) + return ipfs_message + + +class TmManager(BaseBehaviour): + """Util class to be used for managing the tendermint node.""" + + _active_generator: Optional[Generator] = None + _hard_reset_sleep = 20.0 # 20s + _max_reset_retry = 5 + + # TODO: TmManager is not a BaseBehaviour. It should be + # redesigned! + matching_round = Type[AbstractRound] + + def __init__(self, **kwargs: Any): + """Initialize the `TmManager`.""" + super().__init__(**kwargs) + # whether the initiation of a tm fix has been logged + self.informed: bool = False + self.acn_communication_attempted: bool = False + + def async_act(self) -> Generator: + """The behaviour act.""" + self.context.logger.error( + f"{type(self).__name__}'s async_act was called. " + f"This is not allowed as this class is not a behaviour. " + f"Exiting the agent." + ) + error_code = 1 + yield + sys.exit(error_code) + + @property + def is_acting(self) -> bool: + """This method returns whether there is an active fix being applied.""" + return self._active_generator is not None + + @property + def hard_reset_sleep(self) -> float: + """ + Amount of time to sleep before and after performing a hard reset. + + We don't need to wait for half the reset pause duration, like in normal cases where we perform a hard reset. + + :returns: the amount of time to sleep in seconds + """ + return self._hard_reset_sleep + + def _gentle_reset(self) -> Generator[None, None, None]: + """Perform a gentle reset of the Tendermint node.""" + self.context.logger.debug("Performing a gentle reset...") + request_message, http_dialogue = self._build_http_request_message( + "GET", + self.params.tendermint_com_url + "/gentle_reset", + ) + yield from self._do_request(request_message, http_dialogue) + + def _handle_unhealthy_tm(self) -> Generator: + """This method handles the case when the tendermint node is unhealthy.""" + if not self.informed: + self.context.logger.warning( + "The local deadline for the next `begin_block` request from the Tendermint node has expired! " + "Trying to reset local Tendermint node as there could be something wrong with the communication." + ) + self.informed = True + + if not self.gentle_reset_attempted: + self.gentle_reset_attempted = True + yield from self._gentle_reset() + yield from self._check_sync() + return + + is_multi_agent_service = self.synchronized_data.max_participants > 1 + if is_multi_agent_service: + # since we have reached this point, that means that the cause of blocks not being received + # cannot be fixed with a simple gentle reset, + # therefore, we request the recovery parameters via the ACN, and if we succeed, we use them to recover + # we do not need to request the recovery parameters if this is a single-agent service + acn_communication_success = yield from self.request_recovery_params( + should_log=not self.acn_communication_attempted + ) + if not acn_communication_success: + if not self.acn_communication_attempted: + self.context.logger.error( + "Failed to get the recovery parameters via the ACN. Cannot reset Tendermint." + ) + self.acn_communication_attempted = True + return + + recovery_params = self.shared_state.tm_recovery_params + self.round_sequence.reset_state( + restart_from_round=recovery_params.reset_from_round, + round_count=recovery_params.round_count, + serialized_db_state=recovery_params.serialized_db_state, + ) + + for _ in range(self._max_reset_retry): + reset_successfully = yield from self.reset_tendermint_with_wait( + on_startup=True, + is_recovery=True, + ) + if reset_successfully: + self.context.logger.info("Tendermint reset was successfully performed.") + # we sleep to give some time for tendermint to start sending us blocks + # otherwise we might end-up assuming that tendermint is still not working. + # Note that the wait_from_last_timestamp() in reset_tendermint_with_wait() + # doesn't guarantee us this, since the block stall deadline is greater than the + # hard_reset_sleep, 60s vs 20s. In other words, we haven't received a block for at + # least 60s, so wait_from_last_timestamp() will return immediately. + # By setting "on_startup" to True in the reset_tendermint_with_wait() call above, + # wait_from_last_timestamp() will not be called at all. + yield from self.sleep(self.hard_reset_sleep) + self.gentle_reset_attempted = False + return + + self.context.logger.error("Failed to reset tendermint.") + + def _get_reset_params(self, default: bool) -> Optional[Dict[str, str]]: + """ + Get the parameters for a hard reset request when trying to recover agent <-> tendermint communication. + + :param default: ignored for this use case. + :returns: the reset params. + """ + # we get the params from the latest successful reset, if they are not available, + # i.e. no successful reset has been performed, we return None. + # Returning None means default params will be used. + return self.shared_state.tm_recovery_params.reset_params + + def get_callback_request(self) -> Callable[[Message, "BaseBehaviour"], None]: + """Wrapper for callback_request(), overridden to remove checks not applicable to TmManager.""" + + def callback_request( + message: Message, _current_behaviour: BaseBehaviour + ) -> None: + """ + This method gets called when a response for a prior request is received. + + Overridden to remove the check that checks whether the behaviour that made the request is still active. + The received message gets passed to the behaviour that invoked it, in this case it's always the TmManager. + + :param message: the response. + :param _current_behaviour: not used, left in to satisfy the interface. + :return: none + """ + if self.state == AsyncBehaviour.AsyncState.WAITING_MESSAGE: + self.try_send(message) + else: + self.context.logger.warning( + "Could not send message to TmManager: %s", message + ) + + return callback_request + + def try_fix(self) -> None: + """This method tries to fix an unhealthy tendermint node.""" + if self._active_generator is None: + # There is no active generator set, we need to create one. + # A generator being active means that a reset operation is + # being performed. + self._active_generator = self._handle_unhealthy_tm() + try: + # if the behaviour is waiting for a message + # we check whether one has arrived, and if it has + # we send it to the generator. + if self.state == self.AsyncState.WAITING_MESSAGE: + if self.is_notified: + self._active_generator.send(self.received_message) + self._on_sent_message() + # note that if the behaviour is waiting for + # a message, we deliberately don't send a tick + # this was done to have consistency between + # the act here, and acts on normal AsyncBehaviours + return + # this will run the active generator until + # the first yield statement is encountered + self._active_generator.send(None) + + except StopIteration: + # the generator is finished + self.context.logger.debug("Applying tendermint fix finished.") + self._active_generator = None + # the following is required because the message + # 'tick' might be the last one the generator needs + # to complete. In that scenario, we need to call + # the callback here + if self.is_notified: + self._on_sent_message() + + +class DegenerateBehaviour(BaseBehaviour, ABC): + """An abstract matching behaviour for final and degenerate rounds.""" + + matching_round: Type[AbstractRound] + is_degenerate: bool = True + sleep_time_before_exit = 5.0 + + def async_act(self) -> Generator: + """Exit the agent with error when a degenerate round is reached.""" + self.context.logger.error( + "The execution reached a degenerate behaviour. " + "This means a degenerate round has been reached during " + "the execution of the ABCI application. Please check the " + "functioning of the ABCI app." + ) + self.context.logger.error( + f"Sleeping {self.sleep_time_before_exit} seconds before exiting." + ) + yield from self.sleep(self.sleep_time_before_exit) + error_code = 1 + sys.exit(error_code) + + +def make_degenerate_behaviour( + round_cls: Type[AbstractRound], +) -> Type[DegenerateBehaviour]: + """Make a degenerate behaviour class.""" + + class NewDegenerateBehaviour(DegenerateBehaviour): + """A newly defined degenerate behaviour class.""" + + matching_round = round_cls + + new_behaviour_cls = NewDegenerateBehaviour + new_behaviour_cls.__name__ = f"DegenerateBehaviour_{round_cls.auto_round_id()}" # pylint: disable=attribute-defined-outside-init + return new_behaviour_cls diff --git a/packages/valory/skills/abstract_round_abci/behaviours.py b/packages/valory/skills/abstract_round_abci/behaviours.py new file mode 100644 index 0000000..fcf69ea --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/behaviours.py @@ -0,0 +1,409 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the behaviours for the 'abstract_round_abci' skill.""" + +from abc import ABC, ABCMeta +from collections import defaultdict +from dataclasses import asdict +from typing import ( + AbstractSet, + Any, + Dict, + Generator, + Generic, + List, + Optional, + Set, + Tuple, + Type, + cast, +) + +from aea.skills.base import Behaviour + +from packages.valory.skills.abstract_round_abci.base import ( + ABCIAppInternalError, + AbciApp, + AbstractRound, + EventType, + PendingOffencesPayload, + PendingOffencesRound, + PendingOffense, + RoundSequence, +) +from packages.valory.skills.abstract_round_abci.behaviour_utils import ( + BaseBehaviour, + TmManager, + make_degenerate_behaviour, +) +from packages.valory.skills.abstract_round_abci.models import SharedState + + +SLASHING_BACKGROUND_BEHAVIOUR_ID = "slashing_check_behaviour" +TERMINATION_BACKGROUND_BEHAVIOUR_ID = "background_behaviour" + + +BehaviourType = Type[BaseBehaviour] +Action = Optional[str] +TransitionFunction = Dict[BehaviourType, Dict[Action, BehaviourType]] + + +class _MetaRoundBehaviour(ABCMeta): + """A metaclass that validates AbstractRoundBehaviour's attributes.""" + + are_background_behaviours_set: bool = False + + def __new__(mcs, name: str, bases: Tuple, namespace: Dict, **kwargs: Any) -> Type: # type: ignore + """Initialize the class.""" + new_cls = super().__new__(mcs, name, bases, namespace, **kwargs) + + if ABC in bases: + # abstract class, return + return new_cls + if not issubclass(new_cls, AbstractRoundBehaviour): + # the check only applies to AbstractRoundBehaviour subclasses + return new_cls + + mcs.are_background_behaviours_set = bool( + new_cls.background_behaviours_cls - {PendingOffencesBehaviour} + ) + mcs._check_consistency(cast(AbstractRoundBehaviour, new_cls)) + return new_cls + + @classmethod + def _check_consistency(mcs, behaviour_cls: "AbstractRoundBehaviour") -> None: + """Check consistency of class attributes.""" + mcs._check_all_required_classattributes_are_set(behaviour_cls) + mcs._check_behaviour_id_uniqueness(behaviour_cls) + mcs._check_initial_behaviour_in_set_of_behaviours(behaviour_cls) + mcs._check_matching_round_consistency(behaviour_cls) + + @classmethod + def _check_all_required_classattributes_are_set( + mcs, behaviour_cls: "AbstractRoundBehaviour" + ) -> None: + """Check that all the required class attributes are set.""" + try: + _ = behaviour_cls.abci_app_cls + _ = behaviour_cls.behaviours + _ = behaviour_cls.initial_behaviour_cls + except AttributeError as e: + raise ABCIAppInternalError(*e.args) from None + + @classmethod + def _check_behaviour_id_uniqueness( + mcs, behaviour_cls: "AbstractRoundBehaviour" + ) -> None: + """Check that behaviour ids are unique across behaviours.""" + behaviour_id_to_behaviour = defaultdict(lambda: []) + for behaviour_class in behaviour_cls.behaviours: + behaviour_id_to_behaviour[behaviour_class.auto_behaviour_id()].append( + behaviour_class + ) + if len(behaviour_id_to_behaviour[behaviour_class.auto_behaviour_id()]) > 1: + behaviour_classes_names = [ + _behaviour_cls.__name__ + for _behaviour_cls in behaviour_id_to_behaviour[ + behaviour_class.auto_behaviour_id() + ] + ] + raise ABCIAppInternalError( + f"behaviours {behaviour_classes_names} have the same behaviour id '{behaviour_class.auto_behaviour_id()}'" + ) + + @classmethod + def _check_matching_round_consistency( + mcs, behaviour_cls: "AbstractRoundBehaviour" + ) -> None: + """Check that matching rounds are: (1) unique across behaviour, and (2) covering.""" + matching_bg_round_classes = { + behaviour_cls.matching_round + for behaviour_cls in behaviour_cls.background_behaviours_cls + } + round_to_behaviour: Dict[Type[AbstractRound], List[BehaviourType]] = { + round_cls: [] + for round_cls in behaviour_cls.abci_app_cls.get_all_round_classes( + matching_bg_round_classes, + mcs.are_background_behaviours_set, + ) + } + + # check uniqueness + for b in behaviour_cls.behaviours: + behaviours = round_to_behaviour.get(b.matching_round, None) + if behaviours is None: + raise ABCIAppInternalError( + f"Behaviour {b.behaviour_id!r} specifies unknown {b.matching_round!r} as a matching round. " + "Please make sure that the round is implemented and belongs to the FSM. " + f"If {b.behaviour_id!r} is a background behaviour, please make sure that it is set correctly, " + f"by overriding the corresponding attribute of the chained skill's behaviour." + ) + behaviours.append(b) + if len(behaviours) > 1: + behaviour_cls_ids = [ + behaviour_cls_.auto_behaviour_id() for behaviour_cls_ in behaviours + ] + raise ABCIAppInternalError( + f"behaviours {behaviour_cls_ids} have the same matching round '{b.matching_round.auto_round_id()}'" + ) + + # check covering + for round_cls, behaviours in round_to_behaviour.items(): + if round_cls in behaviour_cls.abci_app_cls.final_states: + if len(behaviours) != 0: + raise ABCIAppInternalError( + f"round {round_cls.auto_round_id()} is a final round it shouldn't have any matching behaviours." + ) + elif len(behaviours) == 0: + raise ABCIAppInternalError( + f"round {round_cls.auto_round_id()} is not a matching round of any behaviour" + ) + + @classmethod + def _check_initial_behaviour_in_set_of_behaviours( + mcs, behaviour_cls: "AbstractRoundBehaviour" + ) -> None: + """Check the initial behaviour is in the set of behaviours.""" + if behaviour_cls.initial_behaviour_cls not in behaviour_cls.behaviours: + raise ABCIAppInternalError( + f"initial behaviour {behaviour_cls.initial_behaviour_cls.auto_behaviour_id()} is not in the set of behaviours" + ) + + +class PendingOffencesBehaviour(BaseBehaviour): + """A behaviour responsible for checking whether there are any pending offences.""" + + matching_round = PendingOffencesRound + + @property + def round_sequence(self) -> RoundSequence: + """Get the round sequence from the shared state.""" + return cast(SharedState, self.context.state).round_sequence + + @property + def pending_offences(self) -> Set[PendingOffense]: + """Get the pending offences from the round sequence.""" + return self.round_sequence.pending_offences + + def has_pending_offences(self) -> bool: + """Check if there are any pending offences.""" + return bool(len(self.pending_offences)) + + def async_act(self) -> Generator: + """ + Checks the pending offences. + + This behaviour simply checks if the set of pending offences is not empty. + When it’s not empty, it pops the offence from the set, and sends it to the rest of the agents via a payload + + :return: None + :yield: None + """ + yield from self.wait_for_condition(self.has_pending_offences) + offence = self.pending_offences.pop() + offence_detected_log = ( + f"An offence of type {offence.offense_type.name} has been detected " + f"for agent with address {offence.accused_agent_address} during round {offence.round_count}. " + ) + offence_expiration = offence.last_transition_timestamp + offence.time_to_live + last_timestamp = self.round_sequence.last_round_transition_timestamp + + if offence_expiration < last_timestamp.timestamp(): + ignored_log = "Offence will be ignored as it has expired." + self.context.logger.info(offence_detected_log + ignored_log) + return + + sharing_log = "Sharing offence with the other agents." + self.context.logger.info(offence_detected_log + sharing_log) + + payload = PendingOffencesPayload( + self.context.agent_address, *asdict(offence).values() + ) + yield from self.send_a2a_transaction(payload) + yield from self.wait_until_round_end() + self.set_done() + + +class AbstractRoundBehaviour( # pylint: disable=too-many-instance-attributes + Behaviour, ABC, Generic[EventType], metaclass=_MetaRoundBehaviour +): + """This behaviour implements an abstract round behaviour.""" + + abci_app_cls: Type[AbciApp[EventType]] + behaviours: AbstractSet[BehaviourType] + initial_behaviour_cls: BehaviourType + background_behaviours_cls: Set[BehaviourType] = {PendingOffencesBehaviour} # type: ignore + + def __init__(self, **kwargs: Any) -> None: + """Initialize the behaviour.""" + super().__init__(**kwargs) + self._behaviour_id_to_behaviours: Dict[ + str, BehaviourType + ] = self._get_behaviour_id_to_behaviour_mapping(self.behaviours) + self._round_to_behaviour: Dict[ + Type[AbstractRound], BehaviourType + ] = self._get_round_to_behaviour_mapping(self.behaviours) + + self.current_behaviour: Optional[BaseBehaviour] = None + self.background_behaviours: Set[BaseBehaviour] = set() + self.tm_manager: Optional[TmManager] = None + # keep track of last round height so to detect changes + self._last_round_height = 0 + + # this variable remembers the actual next transition + # when we cannot preemptively interrupt the current behaviour + # because it has not a matching round. + self._next_behaviour_cls: Optional[BehaviourType] = None + + @classmethod + def _get_behaviour_id_to_behaviour_mapping( + cls, behaviours: AbstractSet[BehaviourType] + ) -> Dict[str, BehaviourType]: + """Get behaviour id to behaviour mapping.""" + result: Dict[str, BehaviourType] = {} + for behaviour_cls in behaviours: + behaviour_id = behaviour_cls.auto_behaviour_id() + if behaviour_id in result: + raise ValueError( + f"cannot have two behaviours with the same id; got {behaviour_cls} and {result[behaviour_id]} both with id '{behaviour_id}'" + ) + result[behaviour_id] = behaviour_cls + return result + + @classmethod + def _get_round_to_behaviour_mapping( + cls, behaviours: AbstractSet[BehaviourType] + ) -> Dict[Type[AbstractRound], BehaviourType]: + """Get round-to-behaviour mapping.""" + result: Dict[Type[AbstractRound], BehaviourType] = {} + for behaviour_cls in behaviours: + round_cls = behaviour_cls.matching_round + if round_cls in result: + raise ValueError( + f"the behaviours '{behaviour_cls.auto_behaviour_id()}' and '{result[round_cls].auto_behaviour_id()}' point to the same matching round '{round_cls.auto_round_id()}'" + ) + result[round_cls] = behaviour_cls + + # iterate over rounds and map final (i.e. degenerate) rounds + # to the degenerate behaviour class + for final_round_cls in cls.abci_app_cls.final_states: + new_degenerate_behaviour = make_degenerate_behaviour(final_round_cls) + new_degenerate_behaviour.matching_round = final_round_cls + result[final_round_cls] = new_degenerate_behaviour + + return result + + def instantiate_behaviour_cls(self, behaviour_cls: BehaviourType) -> BaseBehaviour: + """Instantiate the behaviours class.""" + return behaviour_cls( + name=behaviour_cls.auto_behaviour_id(), skill_context=self.context + ) + + def _setup_background(self) -> None: + """Set up the background behaviours.""" + params = cast(BaseBehaviour, self.current_behaviour).params + for background_cls in self.background_behaviours_cls: + background_cls = cast(Type[BaseBehaviour], background_cls) + + if ( + not params.use_termination + and background_cls.auto_behaviour_id() + == TERMINATION_BACKGROUND_BEHAVIOUR_ID + ) or ( + not params.use_slashing + and background_cls.auto_behaviour_id() + == SLASHING_BACKGROUND_BEHAVIOUR_ID + or background_cls == PendingOffencesBehaviour + ): + # comparing with the behaviour id is not entirely safe, as there is a potential for conflicts + # if a user creates a behaviour with the same name + continue + + self.background_behaviours.add( + self.instantiate_behaviour_cls(background_cls) + ) + + def setup(self) -> None: + """Set up the behaviours.""" + self.current_behaviour = self.instantiate_behaviour_cls( + self.initial_behaviour_cls + ) + self.tm_manager = self.instantiate_behaviour_cls(TmManager) # type: ignore + self._setup_background() + + def teardown(self) -> None: + """Tear down the behaviour""" + + def _background_act(self) -> None: + """Call the act wrapper for the background behaviours.""" + for behaviour in self.background_behaviours: + behaviour.act_wrapper() + + def act(self) -> None: + """Implement the behaviour.""" + tm_manager = cast(TmManager, self.tm_manager) + if tm_manager.tm_communication_unhealthy or tm_manager.is_acting: + # tendermint is not healthy, or we are already applying a fix. + # try_fix() internally uses generators, that's why it's relevant + # to know whether a fix is already being applied. + # It might happen that tendermint is healthy, but the fix is not yet finished. + tm_manager.try_fix() + return + + tm_manager.informed = False + tm_manager.acn_communication_attempted = False + self._process_current_round() + if self.current_behaviour is None: + return + + self.current_behaviour.act_wrapper() + if self.current_behaviour.is_done(): + self.current_behaviour.clean_up() + self.current_behaviour = None + + self._background_act() + + def _process_current_round(self) -> None: + """Process current ABCIApp round.""" + current_round_height = self.context.state.round_sequence.current_round_height + if ( + self.current_behaviour is not None + and self._last_round_height == current_round_height + ): + # round has not changed - do nothing + return + self._last_round_height = current_round_height + current_round_cls = type(self.context.state.round_sequence.current_round) + + # each round has a behaviour associated to it + next_behaviour_cls = self._round_to_behaviour[current_round_cls] + + # stop the current behaviour and replace it with the new behaviour + if self.current_behaviour is not None: + current_behaviour = cast(BaseBehaviour, self.current_behaviour) + current_behaviour.clean_up() + current_behaviour.stop() + self.context.logger.debug( + "overriding transition: current behaviour: '%s', next behaviour: '%s'", + self.current_behaviour.behaviour_id if self.current_behaviour else None, + next_behaviour_cls.auto_behaviour_id(), + ) + + self.current_behaviour = self.instantiate_behaviour_cls(next_behaviour_cls) diff --git a/packages/valory/skills/abstract_round_abci/common.py b/packages/valory/skills/abstract_round_abci/common.py new file mode 100644 index 0000000..c6ee52e --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/common.py @@ -0,0 +1,231 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the behaviours, round and payloads for the 'abstract_round_abci' skill.""" + +import hashlib +import random +from abc import ABC +from math import floor +from typing import Any, Dict, Generator, List, Optional, Type, Union, cast + +from packages.valory.protocols.ledger_api.message import LedgerApiMessage +from packages.valory.skills.abstract_round_abci.base import BaseTxPayload +from packages.valory.skills.abstract_round_abci.behaviour_utils import BaseBehaviour +from packages.valory.skills.abstract_round_abci.utils import VerifyDrand + + +RandomnessObservation = Optional[Dict[str, Union[str, int]]] + + +drand_check = VerifyDrand() + + +def random_selection(elements: List[Any], randomness: float) -> str: + """ + Select a random element from a list. + + :param: elements: a list of elements to choose among + :param: randomness: a random number in the [0,1) interval + :return: a randomly chosen element + """ + if not elements: + raise ValueError("No elements to randomly select among") + if randomness < 0 or randomness >= 1: + raise ValueError("Randomness should lie in the [0,1) interval") + random_position = floor(randomness * len(elements)) + return elements[random_position] + + +class RandomnessBehaviour(BaseBehaviour, ABC): + """Behaviour to collect randomness values from DRAND service for keeper agent selection.""" + + payload_class: Type[BaseTxPayload] + + def failsafe_randomness( + self, + ) -> Generator[None, None, RandomnessObservation]: + """ + This methods provides a failsafe for randomness retrieval. + + :return: derived randomness + :yields: derived randomness + """ + ledger_api_response = yield from self.get_ledger_api_response( + performative=LedgerApiMessage.Performative.GET_STATE, # type: ignore + ledger_callable="get_block", + block_identifier="latest", + ) + + if ( + ledger_api_response.performative == LedgerApiMessage.Performative.ERROR + or "hash" not in ledger_api_response.state.body + ): + return None + + randomness = hashlib.sha256( + cast(str, ledger_api_response.state.body.get("hash")).encode() + + str(self.params.service_id).encode() + ).hexdigest() + return {"randomness": randomness, "round": 0} + + def get_randomness_from_api( + self, + ) -> Generator[None, None, RandomnessObservation]: + """Retrieve randomness from given api specs.""" + api_specs = self.context.randomness_api.get_spec() + response = yield from self.get_http_response( + method=api_specs["method"], + url=api_specs["url"], + ) + observation = self.context.randomness_api.process_response(response) + if observation is not None: + self.context.logger.info("Verifying DRAND values...") + check, error = drand_check.verify(observation, self.params.drand_public_key) + if check: + self.context.logger.info("DRAND check successful.") + else: + self.context.logger.error(f"DRAND check failed, {error}.") + return None + return observation + + def async_act(self) -> Generator: + """ + Retrieve randomness from API. + + Steps: + - Do a http request to the API. + - Retry until receiving valid values for randomness or retries exceed. + - If retrieved values are valid continue else generate randomness from chain. + """ + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + if self.context.randomness_api.is_retries_exceeded(): + self.context.logger.warning("Cannot retrieve randomness from api.") + self.context.logger.info("Generating randomness from chain...") + observation = yield from self.failsafe_randomness() + if observation is None: + self.context.logger.error( + "Could not generate randomness from chain!" + ) + return + else: + self.context.logger.info("Retrieving DRAND values from api...") + observation = yield from self.get_randomness_from_api() + self.context.logger.info(f"Retrieved DRAND values: {observation}.") + + if observation: + payload = self.payload_class( # type: ignore + self.context.agent_address, + round_id=observation["round"], + randomness=observation["randomness"], + ) + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + yield from self.send_a2a_transaction(payload) + yield from self.wait_until_round_end() + + self.set_done() + else: + self.context.logger.error( + f"Could not get randomness from {self.context.randomness_api.api_id}" + ) + yield from self.sleep( + self.context.randomness_api.retries_info.suggested_sleep_time + ) + self.context.randomness_api.increment_retries() + + def clean_up(self) -> None: + """ + Clean up the resources due to a 'stop' event. + + It can be optionally implemented by the concrete classes. + """ + self.context.randomness_api.reset_retries() + + +class SelectKeeperBehaviour(BaseBehaviour, ABC): + """Select the keeper agent.""" + + payload_class: Type[BaseTxPayload] + + def _select_keeper(self) -> str: + """ + Select a new keeper randomly. + + 1. Sort the list of participants who are not blacklisted as keepers. + 2. Randomly shuffle it. + 3. Pick the first keeper in order. + 4. If he has already been selected, pick the next one. + + :return: the selected keeper's address. + """ + # Get all the participants who have not been blacklisted as keepers + non_blacklisted = ( + self.synchronized_data.participants + - self.synchronized_data.blacklisted_keepers + ) + if not non_blacklisted: + raise RuntimeError( + "Cannot continue if all the keepers have been blacklisted!" + ) + + # Sorted list of participants who are not blacklisted as keepers + relevant_set = sorted(list(non_blacklisted)) + + # Random seeding and shuffling of the set + random.seed(self.synchronized_data.keeper_randomness) + random.shuffle(relevant_set) + + # If the keeper is not set yet, pick the first address + keeper_address = relevant_set[0] + + # If the keeper has been already set, select the next. + if ( + self.synchronized_data.is_keeper_set + and len(self.synchronized_data.participants) > 1 + ): + old_keeper_index = relevant_set.index( + self.synchronized_data.most_voted_keeper_address + ) + keeper_address = relevant_set[(old_keeper_index + 1) % len(relevant_set)] + + self.context.logger.info(f"Selected a new keeper: {keeper_address}.") + + return keeper_address + + def async_act(self) -> Generator: + """ + Do the action. + + Steps: + - Select a keeper randomly. + - Send the transaction with the keeper and wait for it to be mined. + - Wait until ABCI application transitions to the next round. + - Go to the next behaviour state (set done event). + """ + + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + payload = self.payload_class( # type: ignore + self.context.agent_address, self._select_keeper() + ) + + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + yield from self.send_a2a_transaction(payload) + yield from self.wait_until_round_end() + + self.set_done() diff --git a/packages/valory/skills/abstract_round_abci/dialogues.py b/packages/valory/skills/abstract_round_abci/dialogues.py new file mode 100644 index 0000000..7f0b53a --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/dialogues.py @@ -0,0 +1,368 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the classes required for dialogue management.""" + +from typing import Any, Optional, Type + +from aea.exceptions import enforce +from aea.helpers.transaction.base import Terms +from aea.protocols.base import Address, Message +from aea.protocols.dialogue.base import Dialogue as BaseDialogue +from aea.protocols.dialogue.base import DialogueLabel as BaseDialogueLabel +from aea.skills.base import Model + +from packages.open_aea.protocols.signing.dialogues import ( + SigningDialogue as BaseSigningDialogue, +) +from packages.open_aea.protocols.signing.dialogues import ( + SigningDialogues as BaseSigningDialogues, +) +from packages.valory.protocols.abci.dialogues import AbciDialogue as BaseAbciDialogue +from packages.valory.protocols.abci.dialogues import AbciDialogues as BaseAbciDialogues +from packages.valory.protocols.contract_api import ContractApiMessage +from packages.valory.protocols.contract_api.dialogues import ( + ContractApiDialogue as BaseContractApiDialogue, +) +from packages.valory.protocols.contract_api.dialogues import ( + ContractApiDialogues as BaseContractApiDialogues, +) +from packages.valory.protocols.http.dialogues import HttpDialogue as BaseHttpDialogue +from packages.valory.protocols.http.dialogues import HttpDialogues as BaseHttpDialogues +from packages.valory.protocols.ipfs.dialogues import IpfsDialogue as BaseIpfsDialogue +from packages.valory.protocols.ipfs.dialogues import IpfsDialogues as BaseIpfsDialogues +from packages.valory.protocols.ledger_api import LedgerApiMessage +from packages.valory.protocols.ledger_api.dialogues import ( + LedgerApiDialogue as BaseLedgerApiDialogue, +) +from packages.valory.protocols.ledger_api.dialogues import ( + LedgerApiDialogues as BaseLedgerApiDialogues, +) +from packages.valory.protocols.tendermint.dialogues import ( + TendermintDialogue as BaseTendermintDialogue, +) +from packages.valory.protocols.tendermint.dialogues import ( + TendermintDialogues as BaseTendermintDialogues, +) + + +AbciDialogue = BaseAbciDialogue + + +class AbciDialogues(Model, BaseAbciDialogues): + """The dialogues class keeps track of all dialogues.""" + + def __init__(self, **kwargs: Any) -> None: + """ + Initialize dialogues. + + :param kwargs: keyword arguments + """ + Model.__init__(self, **kwargs) + + def role_from_first_message( # pylint: disable=unused-argument + message: Message, receiver_address: Address + ) -> BaseDialogue.Role: + """Infer the role of the agent from an incoming/outgoing first message + + :param message: an incoming/outgoing first message + :param receiver_address: the address of the receiving agent + :return: The role of the agent + """ + return AbciDialogue.Role.CLIENT + + BaseAbciDialogues.__init__( + self, + self_address=str(self.skill_id), + role_from_first_message=role_from_first_message, + ) + + +HttpDialogue = BaseHttpDialogue + + +class HttpDialogues(Model, BaseHttpDialogues): + """This class keeps track of all http dialogues.""" + + def __init__(self, **kwargs: Any) -> None: + """ + Initialize dialogues. + + :param kwargs: keyword arguments + """ + Model.__init__(self, **kwargs) + + def role_from_first_message( # pylint: disable=unused-argument + message: Message, receiver_address: Address + ) -> BaseDialogue.Role: + """Infer the role of the agent from an incoming/outgoing first message + + :param message: an incoming/outgoing first message + :param receiver_address: the address of the receiving agent + :return: The role of the agent + """ + return BaseHttpDialogue.Role.CLIENT + + BaseHttpDialogues.__init__( + self, + self_address=str(self.skill_id), + role_from_first_message=role_from_first_message, + ) + + +SigningDialogue = BaseSigningDialogue + + +class SigningDialogues(Model, BaseSigningDialogues): + """This class keeps track of all signing dialogues.""" + + def __init__(self, **kwargs: Any) -> None: + """ + Initialize dialogues. + + :param kwargs: keyword arguments + """ + Model.__init__(self, **kwargs) + + def role_from_first_message( # pylint: disable=unused-argument + message: Message, receiver_address: Address + ) -> BaseDialogue.Role: + """Infer the role of the agent from an incoming/outgoing first message + + :param message: an incoming/outgoing first message + :param receiver_address: the address of the receiving agent + :return: The role of the agent + """ + return BaseSigningDialogue.Role.SKILL + + BaseSigningDialogues.__init__( + self, + self_address=str(self.skill_id), + role_from_first_message=role_from_first_message, + ) + + +class LedgerApiDialogue( # pylint: disable=too-few-public-methods + BaseLedgerApiDialogue +): + """The dialogue class maintains state of a dialogue and manages it.""" + + __slots__ = ("_terms",) + + def __init__( + self, + dialogue_label: BaseDialogueLabel, + self_address: Address, + role: BaseDialogue.Role, + message_class: Type[LedgerApiMessage] = LedgerApiMessage, + ) -> None: + """ + Initialize a dialogue. + + :param dialogue_label: the identifier of the dialogue + :param self_address: the address of the entity for whom this dialogue is maintained + :param role: the role of the agent this dialogue is maintained for + :param message_class: the message class + """ + BaseLedgerApiDialogue.__init__( + self, + dialogue_label=dialogue_label, + self_address=self_address, + role=role, + message_class=message_class, + ) + self._terms = None # type: Optional[Terms] + + @property + def terms(self) -> Terms: + """Get the terms.""" + if self._terms is None: + raise ValueError("Terms not set!") + return self._terms + + @terms.setter + def terms(self, terms: Terms) -> None: + """Set the terms.""" + enforce(self._terms is None, "Terms already set!") + self._terms = terms + + +class LedgerApiDialogues(Model, BaseLedgerApiDialogues): + """The dialogues class keeps track of all dialogues.""" + + def __init__(self, **kwargs: Any) -> None: + """ + Initialize dialogues. + + :param kwargs: keyword arguments + """ + Model.__init__(self, **kwargs) + + def role_from_first_message( # pylint: disable=unused-argument + message: Message, receiver_address: Address + ) -> BaseDialogue.Role: + """Infer the role of the agent from an incoming/outgoing first message + + :param message: an incoming/outgoing first message + :param receiver_address: the address of the receiving agent + :return: The role of the agent + """ + return BaseLedgerApiDialogue.Role.AGENT + + BaseLedgerApiDialogues.__init__( + self, + self_address=str(self.skill_id), + role_from_first_message=role_from_first_message, + dialogue_class=LedgerApiDialogue, + ) + + +class ContractApiDialogue( # pylint: disable=too-few-public-methods + BaseContractApiDialogue +): + """The dialogue class maintains state of a dialogue and manages it.""" + + __slots__ = ("_terms",) + + def __init__( + self, + dialogue_label: BaseDialogueLabel, + self_address: Address, + role: BaseDialogue.Role, + message_class: Type[ContractApiMessage] = ContractApiMessage, + ) -> None: + """ + Initialize a dialogue. + + :param dialogue_label: the identifier of the dialogue + :param self_address: the address of the entity for whom this dialogue is maintained + :param role: the role of the agent this dialogue is maintained for + :param message_class: the message class + """ + BaseContractApiDialogue.__init__( + self, + dialogue_label=dialogue_label, + self_address=self_address, + role=role, + message_class=message_class, + ) + self._terms = None # type: Optional[Terms] + + @property + def terms(self) -> Terms: + """Get the terms.""" + if self._terms is None: + raise ValueError("Terms not set!") + return self._terms + + @terms.setter + def terms(self, terms: Terms) -> None: + """Set the terms.""" + enforce(self._terms is None, "Terms already set!") + self._terms = terms + + +class ContractApiDialogues(Model, BaseContractApiDialogues): + """The dialogues class keeps track of all dialogues.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize dialogues.""" + Model.__init__(self, **kwargs) + + def role_from_first_message( # pylint: disable=unused-argument + message: Message, receiver_address: Address + ) -> BaseDialogue.Role: + """Infer the role of the agent from an incoming/outgoing first message + + :param message: an incoming/outgoing first message + :param receiver_address: the address of the receiving agent + :return: The role of the agent + """ + return ContractApiDialogue.Role.AGENT + + BaseContractApiDialogues.__init__( + self, + self_address=str(self.skill_id), + role_from_first_message=role_from_first_message, + dialogue_class=ContractApiDialogue, + ) + + +TendermintDialogue = BaseTendermintDialogue + + +class TendermintDialogues(Model, BaseTendermintDialogues): + """The dialogues class keeps track of all dialogues.""" + + def __init__(self, **kwargs: Any) -> None: + """ + Initialize dialogues. + + :param kwargs: keyword arguments + """ + Model.__init__(self, **kwargs) + + def role_from_first_message( # pylint: disable=unused-argument + message: Message, receiver_address: Address + ) -> BaseDialogue.Role: + """Infer the role of the agent from an incoming/outgoing first message + + :param message: an incoming/outgoing first message + :param receiver_address: the address of the receiving agent + :return: The role of the agent + """ + return TendermintDialogue.Role.AGENT + + BaseTendermintDialogues.__init__( + self, + self_address=self.context.agent_address, + role_from_first_message=role_from_first_message, + ) + + +IpfsDialogue = BaseIpfsDialogue + + +class IpfsDialogues(Model, BaseIpfsDialogues): + """A class to keep track of IPFS dialogues.""" + + def __init__(self, **kwargs: Any) -> None: + """ + Initialize dialogues. + + :param kwargs: keyword arguments + """ + Model.__init__(self, **kwargs) + + def role_from_first_message( # pylint: disable=unused-argument + message: Message, receiver_address: Address + ) -> BaseDialogue.Role: + """Infer the role of the agent from an incoming/outgoing first message + + :param message: an incoming/outgoing first message + :param receiver_address: the address of the receiving agent + :return: The role of the agent + """ + return IpfsDialogue.Role.SKILL + + BaseIpfsDialogues.__init__( + self, + self_address=str(self.skill_id), + role_from_first_message=role_from_first_message, + ) diff --git a/packages/valory/skills/abstract_round_abci/handlers.py b/packages/valory/skills/abstract_round_abci/handlers.py new file mode 100644 index 0000000..88cf072 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/handlers.py @@ -0,0 +1,790 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the handler for the 'abstract_round_abci' skill.""" + +import ipaddress +import json +from abc import ABC +from calendar import timegm +from dataclasses import asdict +from enum import Enum +from typing import Any, Callable, Dict, FrozenSet, List, Optional, cast + +from aea.configurations.data_types import PublicId +from aea.protocols.base import Message +from aea.protocols.dialogue.base import Dialogue, Dialogues +from aea.skills.base import Handler + +from packages.open_aea.protocols.signing import SigningMessage +from packages.valory.protocols.abci import AbciMessage +from packages.valory.protocols.abci.custom_types import Events, ValidatorUpdates +from packages.valory.protocols.contract_api import ContractApiMessage +from packages.valory.protocols.http import HttpMessage +from packages.valory.protocols.ipfs import IpfsMessage +from packages.valory.protocols.ledger_api import LedgerApiMessage +from packages.valory.protocols.tendermint.dialogues import ( + TendermintDialogue, + TendermintDialogues, +) +from packages.valory.protocols.tendermint.message import TendermintMessage +from packages.valory.skills.abstract_abci.handlers import ABCIHandler +from packages.valory.skills.abstract_round_abci.base import ( + ABCIAppInternalError, + AddBlockError, + DEFAULT_PENDING_OFFENCE_TTL, + ERROR_CODE, + LateArrivingTransaction, + OK_CODE, + OffenseType, + PendingOffense, + SignatureNotValidError, + Transaction, + TransactionNotValidError, + TransactionTypeNotRecognizedError, +) +from packages.valory.skills.abstract_round_abci.behaviours import AbstractRoundBehaviour +from packages.valory.skills.abstract_round_abci.dialogues import AbciDialogue +from packages.valory.skills.abstract_round_abci.models import ( + Requests, + SharedState, + TendermintRecoveryParams, +) + + +def exception_to_info_msg(exception: Exception) -> str: + """Transform an exception to an info string message.""" + return f"{exception.__class__.__name__}: {str(exception)}" + + +class ABCIRoundHandler(ABCIHandler): + """ABCI handler.""" + + SUPPORTED_PROTOCOL = AbciMessage.protocol_id + + def info(self, message: AbciMessage, dialogue: AbciDialogue) -> AbciMessage: + """ + Handle the 'info' request. + + As per Tendermint spec (https://github.com/tendermint/spec/blob/038f3e025a19fed9dc96e718b9834ab1b545f136/spec/abci/abci.md#info): + + - Return information about the application state. + - Used to sync Tendermint with the application during a handshake that happens on startup. + - The returned app_version will be included in the Header of every block. + - Tendermint expects last_block_app_hash and last_block_height to be updated during Commit, ensuring that Commit is never called twice for the same block height. + + :param message: the ABCI request. + :param dialogue: the ABCI dialogue. + :return: the response. + """ + # some arbitrary information + info_data = "" + # the application software semantic version + version = "" + # the application protocol version + app_version = 0 + # latest block for which the app has called Commit + last_block_height = self.context.state.round_sequence.height + # latest result of Commit + last_block_app_hash = self.context.state.round_sequence.root_hash + reply = dialogue.reply( + performative=AbciMessage.Performative.RESPONSE_INFO, + target_message=message, + info_data=info_data, + version=version, + app_version=app_version, + last_block_height=last_block_height, + last_block_app_hash=last_block_app_hash, + ) + return cast(AbciMessage, reply) + + def init_chain(self, message: AbciMessage, dialogue: AbciDialogue) -> AbciMessage: + """ + Handle a message of REQUEST_INIT_CHAIN performative. + + As per Tendermint spec (https://github.com/tendermint/spec/blob/038f3e025a19fed9dc96e718b9834ab1b545f136/spec/abci/abci.md#initchain): + + - Called once upon genesis. + - If ResponseInitChain.Validators is empty, the initial validator set will be the RequestInitChain.Validators. + - If ResponseInitChain.Validators is not empty, it will be the initial validator set (regardless of what is in RequestInitChain.Validators). + - This allows the app to decide if it wants to accept the initial validator set proposed by tendermint (ie. in the genesis file), or if it wants to use a different one (perhaps computed based on some application specific information in the genesis file). + + :param message: the ABCI request. + :param dialogue: the ABCI dialogue. + :return: the response. + """ + # Initial validator set (optional). + validators: List = [] + # Get the root hash of the last round transition as the initial application hash. + # If no round transitions have occurred yet, `last_root_hash` returns the hash of the initial abci app's state. + # `init_chain` will be called between resets when restarting again. + app_hash = self.context.state.round_sequence.last_round_transition_root_hash + cast(SharedState, self.context.state).round_sequence.init_chain( + message.initial_height + ) + reply = dialogue.reply( + performative=AbciMessage.Performative.RESPONSE_INIT_CHAIN, + target_message=message, + validators=ValidatorUpdates(validators), + app_hash=app_hash, + ) + return cast(AbciMessage, reply) + + def begin_block(self, message: AbciMessage, dialogue: AbciDialogue) -> AbciMessage: + """Handle the 'begin_block' request.""" + cast(SharedState, self.context.state).round_sequence.begin_block( + message.header, message.byzantine_validators, message.last_commit_info + ) + return super().begin_block(message, dialogue) + + def check_tx(self, message: AbciMessage, dialogue: AbciDialogue) -> AbciMessage: + """Handle the 'check_tx' request.""" + transaction_bytes = message.tx + # check we can decode the transaction + try: + transaction = Transaction.decode(transaction_bytes) + transaction.verify(self.context.default_ledger_id) + cast(SharedState, self.context.state).round_sequence.check_is_finished() + except ( + SignatureNotValidError, + TransactionNotValidError, + TransactionTypeNotRecognizedError, + ) as exception: + self._log_exception(exception) + return self._check_tx_failed( + message, dialogue, exception_to_info_msg(exception) + ) + except LateArrivingTransaction as exception: # pragma: nocover + self.context.logger.debug(exception_to_info_msg(exception)) + return self._check_tx_failed( + message, dialogue, exception_to_info_msg(exception) + ) + + # return check_tx success + reply = dialogue.reply( + performative=AbciMessage.Performative.RESPONSE_CHECK_TX, + target_message=message, + code=OK_CODE, + data=b"", + log="", + info="check_tx succeeded", + gas_wanted=0, + gas_used=0, + events=Events([]), + codespace="", + ) + return cast(AbciMessage, reply) + + def settle_pending_offence( + self, accused_agent_address: Optional[str], invalid: bool + ) -> None: + """Add an invalid pending offence or a no-offence for the given accused agent address, if possible.""" + if accused_agent_address is None: + # only add the offence if we know and can verify the sender, + # otherwise someone could pretend to be someone else, which may lead to wrong punishments + return + + round_sequence = cast(SharedState, self.context.state).round_sequence + + try: + last_round_transition_timestamp = timegm( + round_sequence.last_round_transition_timestamp.utctimetuple() + ) + except ValueError: # pragma: no cover + # do not add an offence if no round transition has been completed yet + return + + offence_type = ( + OffenseType.INVALID_PAYLOAD if invalid else OffenseType.NO_OFFENCE + ) + pending_offense = PendingOffense( + accused_agent_address, + round_sequence.current_round_height, + offence_type, + last_round_transition_timestamp, + DEFAULT_PENDING_OFFENCE_TTL, + ) + round_sequence.add_pending_offence(pending_offense) + + def deliver_tx(self, message: AbciMessage, dialogue: AbciDialogue) -> AbciMessage: + """Handle the 'deliver_tx' request.""" + transaction_bytes = message.tx + round_sequence = cast(SharedState, self.context.state).round_sequence + payload_sender: Optional[str] = None + try: + transaction = Transaction.decode(transaction_bytes) + transaction.verify(self.context.default_ledger_id) + payload_sender = transaction.payload.sender + round_sequence.check_is_finished() + round_sequence.deliver_tx(transaction) + except ( + SignatureNotValidError, + TransactionNotValidError, + TransactionTypeNotRecognizedError, + ) as exception: + self._log_exception(exception) + # the transaction is invalid, it's potentially an offence, so we add it to the list of pending offences + self.settle_pending_offence(payload_sender, invalid=True) + return self._deliver_tx_failed( + message, dialogue, exception_to_info_msg(exception) + ) + except LateArrivingTransaction as exception: # pragma: nocover + self.context.logger.debug(exception_to_info_msg(exception)) + return self._deliver_tx_failed( + message, dialogue, exception_to_info_msg(exception) + ) + + # the invalid payloads' availability window needs to be populated with the negative values as well + self.settle_pending_offence(payload_sender, invalid=False) + + # return deliver_tx success + reply = dialogue.reply( + performative=AbciMessage.Performative.RESPONSE_DELIVER_TX, + target_message=message, + code=OK_CODE, + data=b"", + log="", + info="deliver_tx succeeded", + gas_wanted=0, + gas_used=0, + events=Events([]), + codespace="", + ) + return cast(AbciMessage, reply) + + def end_block(self, message: AbciMessage, dialogue: AbciDialogue) -> AbciMessage: + """Handle the 'end_block' request.""" + self.context.state.round_sequence.tm_height = message.height + cast(SharedState, self.context.state).round_sequence.end_block() + return super().end_block(message, dialogue) + + def commit(self, message: AbciMessage, dialogue: AbciDialogue) -> AbciMessage: + """ + Handle the 'commit' request. + + As per Tendermint spec (https://github.com/tendermint/spec/blob/038f3e025a19fed9dc96e718b9834ab1b545f136/spec/abci/abci.md#commit): + + Empty request meant to signal to the app it can write state transitions to state. + + - Persist the application state. + - Return a Merkle root hash of the application state. + - It's critical that all application instances return the same hash. If not, they will not be able to agree on the next block, because the hash is included in the next block! + + :param message: the ABCI request. + :param dialogue: the ABCI dialogue. + :return: the response. + """ + try: + cast(SharedState, self.context.state).round_sequence.commit() + except AddBlockError as exception: + self._log_exception(exception) + raise exception + # The Merkle root hash of the application state. + data = self.context.state.round_sequence.root_hash + # Blocks below this height may be removed. Defaults to 0 (retain all). + retain_height = 0 + # return commit success + reply = dialogue.reply( + performative=AbciMessage.Performative.RESPONSE_COMMIT, + target_message=message, + data=data, + retain_height=retain_height, + ) + return cast(AbciMessage, reply) + + @classmethod + def _check_tx_failed( + cls, message: AbciMessage, dialogue: AbciDialogue, info: str = "" + ) -> AbciMessage: + """Handle a failed check_tx request.""" + reply = dialogue.reply( + performative=AbciMessage.Performative.RESPONSE_CHECK_TX, + target_message=message, + code=ERROR_CODE, + data=b"", + log="", + info=info, + gas_wanted=0, + gas_used=0, + events=Events([]), + codespace="", + ) + return cast(AbciMessage, reply) + + @classmethod + def _deliver_tx_failed( + cls, message: AbciMessage, dialogue: AbciDialogue, info: str = "" + ) -> AbciMessage: + """Handle a failed deliver_tx request.""" + reply = dialogue.reply( + performative=AbciMessage.Performative.RESPONSE_DELIVER_TX, + target_message=message, + code=ERROR_CODE, + data=b"", + log="", + info=info, + gas_wanted=0, + gas_used=0, + events=Events([]), + codespace="", + ) + return cast(AbciMessage, reply) + + def _log_exception(self, exception: Exception) -> None: + """Log an exception.""" + self.context.logger.error(exception_to_info_msg(exception)) + + +class AbstractResponseHandler(Handler, ABC): + """ + Abstract response Handler. + + This abstract handler works in tandem with the 'Requests' model. + Whenever a message of 'response' type arrives, the handler + tries to dispatch it to a pending request previously registered + in 'Requests' by some other code in the same skill. + + The concrete classes must set the 'allowed_response_performatives' + class attribute to the (frozen)set of performative the developer + wants the handler to handle. + """ + + allowed_response_performatives: FrozenSet[Message.Performative] + + def setup(self) -> None: + """Set up the handler.""" + + def teardown(self) -> None: + """Tear down the handler.""" + + def handle(self, message: Message) -> None: + """ + Handle the response message. + + Steps: + 1. Try to recover the 'dialogues' instance, for the protocol + of this handler, from the skill context. The attribute name used to + read the attribute is computed by '_get_dialogues_attribute_name()' + method. If no dialogues instance is found, log a message and return. + 2. Try to recover the dialogue; if no dialogue is present, log a message + and return. + 3. Check whether the performative is in the set of allowed performative; + if not, log a message and return. + 4. Try to recover the callback of the request associated to the response + from the 'Requests' model; if no callback is present, log a message + and return. + 5. If the above check have passed, then call the callback with the + received message. + + :param message: the message to handle. + """ + protocol_dialogues = self._recover_protocol_dialogues() + if protocol_dialogues is None: + self._handle_missing_dialogues() + return + protocol_dialogues = cast(Dialogues, protocol_dialogues) + + protocol_dialogue = cast(Optional[Dialogue], protocol_dialogues.update(message)) + if protocol_dialogue is None: + self._handle_unidentified_dialogue(message) + return + + if message.performative not in self.allowed_response_performatives: + self._handle_unallowed_performative(message) + return + + request_nonce = protocol_dialogue.dialogue_label.dialogue_reference[0] + ctx_requests = cast(Requests, self.context.requests) + + try: + callback = cast( + Callable, + ctx_requests.request_id_to_callback.pop(request_nonce), + ) + except KeyError as e: + raise ABCIAppInternalError( + f"No callback defined for request with nonce: {request_nonce}" + ) from e + + self._log_message_handling(message) + current_behaviour = cast( + AbstractRoundBehaviour, self.context.behaviours.main + ).current_behaviour + callback(message, current_behaviour) + + def _get_dialogues_attribute_name(self) -> str: + """ + Get dialogues attribute name. + + By convention, the Dialogues model of the skill follows + the template '{protocol_name}_dialogues'. + + Override this method accordingly if the name of hte Dialogues + model is different. + + :return: the dialogues attribute name. + """ + return cast(PublicId, self.SUPPORTED_PROTOCOL).name + "_dialogues" + + def _recover_protocol_dialogues(self) -> Optional[Dialogues]: + """ + Recover protocol dialogues from supported protocol id. + + :return: the dialogues, or None if the dialogues object was not found. + """ + attribute = self._get_dialogues_attribute_name() + return getattr(self.context, attribute, None) + + def _handle_missing_dialogues(self) -> None: + """Handle missing dialogues in context.""" + expected_attribute_name = self._get_dialogues_attribute_name() + self.context.logger.warning( + "Cannot find Dialogues object in skill context with attribute name: %s", + expected_attribute_name, + ) + + def _handle_unidentified_dialogue(self, message: Message) -> None: + """ + Handle an unidentified dialogue. + + :param message: the unidentified message to be handled + """ + self.context.logger.warning( + "Received invalid message: unidentified dialogue. message=%s", message + ) + + def _handle_unallowed_performative(self, message: Message) -> None: + """ + Handle a message with an unallowed response performative. + + Log an error message saying that the handler did not expect requests + but only responses. + + :param message: the message + """ + self.context.logger.warning( + "Received invalid message: unallowed performative. message=%s.", message + ) + + def _log_message_handling(self, message: Message) -> None: + """Log the handling of the message.""" + self.context.logger.debug( + "Calling registered callback with message=%s", message + ) + + +class HttpHandler(AbstractResponseHandler): + """The HTTP response handler.""" + + SUPPORTED_PROTOCOL: Optional[PublicId] = HttpMessage.protocol_id + allowed_response_performatives = frozenset({HttpMessage.Performative.RESPONSE}) + + +class SigningHandler(AbstractResponseHandler): + """Implement the transaction handler.""" + + SUPPORTED_PROTOCOL: Optional[PublicId] = SigningMessage.protocol_id + allowed_response_performatives = frozenset( + { + SigningMessage.Performative.SIGNED_MESSAGE, + SigningMessage.Performative.SIGNED_TRANSACTION, + SigningMessage.Performative.ERROR, + } + ) + + +class LedgerApiHandler(AbstractResponseHandler): + """Implement the ledger handler.""" + + SUPPORTED_PROTOCOL: Optional[PublicId] = LedgerApiMessage.protocol_id + allowed_response_performatives = frozenset( + { + LedgerApiMessage.Performative.BALANCE, + LedgerApiMessage.Performative.RAW_TRANSACTION, + LedgerApiMessage.Performative.TRANSACTION_DIGEST, + LedgerApiMessage.Performative.TRANSACTION_RECEIPT, + LedgerApiMessage.Performative.ERROR, + LedgerApiMessage.Performative.STATE, + } + ) + + +class ContractApiHandler(AbstractResponseHandler): + """Implement the contract api handler.""" + + SUPPORTED_PROTOCOL: Optional[PublicId] = ContractApiMessage.protocol_id + allowed_response_performatives = frozenset( + { + ContractApiMessage.Performative.RAW_TRANSACTION, + ContractApiMessage.Performative.RAW_MESSAGE, + ContractApiMessage.Performative.ERROR, + ContractApiMessage.Performative.STATE, + } + ) + + +class TendermintHandler(Handler): + """ + The Tendermint config-sharing request / response handler. + + This handler is used to share the information necessary + to set up the Tendermint network. The agents use it during + the RegistrationStartupBehaviour, and communicate with + each other over the Agent Communication Network using a + p2p_libp2p or p2p_libp2p_client connection. + + This handler does NOT use the ABCI connection. + """ + + SUPPORTED_PROTOCOL: Optional[PublicId] = TendermintMessage.protocol_id + + class LogMessages(Enum): + """Log messages used in the TendermintHandler""" + + unidentified_dialogue = "Unidentified Tendermint dialogue" + no_addresses_retrieved_yet = "No registered addresses retrieved yet" + not_in_registered_addresses = "Sender not registered for on-chain service" + sending_request_response = "Sending Tendermint request response" + failed_to_parse_address = "Failed to parse Tendermint network address" + failed_to_parse_params = ( + "Failed to parse Tendermint recovery parameters from message" + ) + collected_config_info = "Collected Tendermint config info" + collected_params = "Collected Tendermint recovery parameters" + received_error_without_target_message = ( + "Received error message but could not retrieve target message" + ) + received_error_response = "Received error response" + sending_error_response = "Sending error response" + performative_not_recognized = "Performative not recognized" + + def __str__(self) -> str: # pragma: no cover + """For ease of use in formatted string literals""" + return self.value + + def setup(self) -> None: + """Set up the handler.""" + + def teardown(self) -> None: + """Tear down the handler.""" + + @property + def initial_tm_configs(self) -> Dict[str, Dict[str, Any]]: + """A mapping of the other agents' addresses to their initial Tendermint configuration.""" + return self.context.state.initial_tm_configs + + @initial_tm_configs.setter + def initial_tm_configs(self, configs: Dict[str, Dict[str, Any]]) -> None: + """A mapping of the other agents' addresses to their initial Tendermint configuration.""" + self.context.state.initial_tm_configs = configs + + @property + def dialogues(self) -> Optional[TendermintDialogues]: + """Tendermint config-sharing request / response protocol dialogues""" + + attribute = cast(PublicId, self.SUPPORTED_PROTOCOL).name + "_dialogues" + return getattr(self.context, attribute, None) + + def handle(self, message: Message) -> None: + """Handle incoming Tendermint config-sharing messages""" + + dialogues = cast(TendermintDialogues, self.dialogues) + dialogue = cast(TendermintDialogue, dialogues.update(message)) + + if dialogue is None: + log_message = self.LogMessages.unidentified_dialogue.value + self.context.logger.error(f"{log_message}: {message}") + return + + message = cast(TendermintMessage, message) + handler_name = f"_{message.performative.value}" + handler = getattr(self, handler_name, None) + if handler is None: + log_message = self.LogMessages.performative_not_recognized.value + self.context.logger.error(f"{log_message}: {message}") + return + + handler(message, dialogue) + + def _reply_with_tendermint_error( + self, + message: TendermintMessage, + dialogue: TendermintDialogue, + error_message: str, + ) -> None: + """Reply with Tendermint config-sharing error""" + response = dialogue.reply( + performative=TendermintMessage.Performative.ERROR, + target_message=message, + error_code=TendermintMessage.ErrorCode.INVALID_REQUEST, + error_msg=error_message, + error_data={}, + ) + self.context.outbox.put_message(response) + log_message = self.LogMessages.sending_error_response.value + log_message += f". Received: {message}, replied: {response}" + self.context.logger.error(log_message) + + def _not_registered_error( + self, message: TendermintMessage, dialogue: TendermintDialogue + ) -> None: + """Check if sender is among on-chain registered addresses""" + # do not respond to errors to avoid loops + log_message = self.LogMessages.not_in_registered_addresses.value + self.context.logger.error(f"{log_message}: {message}") + self._reply_with_tendermint_error(message, dialogue, log_message) + + def _check_registered( + self, message: TendermintMessage, dialogue: TendermintDialogue + ) -> bool: + """Check if the sender is registered on-chain and if not, reply with an error""" + others_addresses = self.context.state.acn_container() + if message.sender in others_addresses: + return True + + self._not_registered_error(message, dialogue) + return False + + def _get_genesis_info( + self, message: TendermintMessage, dialogue: TendermintDialogue + ) -> None: + """Handler Tendermint config-sharing request message""" + + if not self._check_registered(message, dialogue): + return + info = self.initial_tm_configs.get(self.context.agent_address, None) + if info is None: + log_message = self.LogMessages.no_addresses_retrieved_yet.value + self.context.logger.info(f"{log_message}: {message}") + self._reply_with_tendermint_error(message, dialogue, log_message) + return + + response = dialogue.reply( + performative=TendermintMessage.Performative.GENESIS_INFO, + target_message=message, + info=json.dumps(info), + ) + self.context.outbox.put_message(message=response) + log_message = self.LogMessages.sending_request_response.value + self.context.logger.info(f"{log_message}: {response}") + + def _get_recovery_params( + self, message: TendermintMessage, dialogue: TendermintDialogue + ) -> None: + """Handle a request message for the recovery parameters.""" + if not self._check_registered(message, dialogue): + return + + shared_state = cast(SharedState, self.context.state) + recovery_params = shared_state.tm_recovery_params + response = dialogue.reply( + performative=TendermintMessage.Performative.RECOVERY_PARAMS, + target_message=message, + params=json.dumps(asdict(recovery_params)), + ) + self.context.outbox.put_message(message=response) + log_message = self.LogMessages.sending_request_response.value + self.context.logger.info(f"{log_message}: {response}") + + def _genesis_info( + self, message: TendermintMessage, dialogue: TendermintDialogue + ) -> None: + """Process Tendermint config-sharing response messages""" + + if not self._check_registered(message, dialogue): + return + + try: # validate message contains a valid address + validator_config = json.loads(message.info) + self.context.logger.info(f"Validator config received: {validator_config}") + hostname = cast(str, validator_config["hostname"]) + if hostname != "localhost" and not hostname.startswith("node"): + ipaddress.ip_network(hostname) + except (KeyError, ValueError) as e: + log_message = self.LogMessages.failed_to_parse_address.value + self.context.logger.error(f"{log_message}: {e} {message}") + self._reply_with_tendermint_error(message, dialogue, log_message) + return + + initial_tm_configs = self.initial_tm_configs + initial_tm_configs[message.sender] = validator_config + self.initial_tm_configs = initial_tm_configs + log_message = self.LogMessages.collected_config_info.value + self.context.logger.info(f"{log_message}: {message}") + dialogues = cast(TendermintDialogues, self.dialogues) + dialogues.dialogue_stats.add_dialogue_endstate( + TendermintDialogue.EndState.COMMUNICATED, dialogue.is_self_initiated + ) + + def _recovery_params( + self, message: TendermintMessage, dialogue: TendermintDialogue + ) -> None: + """Process params-sharing response messages.""" + + if not self._check_registered(message, dialogue): + return + + try: + recovery_params = json.loads(message.params) + shared_state = cast(SharedState, self.context.state) + shared_state.address_to_acn_deliverable[ + message.sender + ] = TendermintRecoveryParams(**recovery_params) + except (json.JSONDecodeError, TypeError) as exc: + log_message = self.LogMessages.failed_to_parse_params.value + self.context.logger.error(f"{log_message}: {exc} {message}") + self._reply_with_tendermint_error(message, dialogue, log_message) + return + + log_message = self.LogMessages.collected_params.value + self.context.logger.info(f"{log_message}: {message}") + dialogues = cast(TendermintDialogues, self.dialogues) + dialogues.dialogue_stats.add_dialogue_endstate( + TendermintDialogue.EndState.COMMUNICATED, dialogue.is_self_initiated + ) + + def _error(self, message: TendermintMessage, dialogue: TendermintDialogue) -> None: + """Handle error message as response""" + + target_message = dialogue.get_message_by_id(message.target) + if not target_message: + log_message = self.LogMessages.received_error_without_target_message.value + self.context.logger.error(log_message) + return + + log_message = self.LogMessages.received_error_response.value + log_message += f". Received: {message}, in reply to: {target_message}" + self.context.logger.error(log_message) + dialogues = cast(TendermintDialogues, self.dialogues) + dialogues.dialogue_stats.add_dialogue_endstate( + TendermintDialogue.EndState.NOT_COMMUNICATED, dialogue.is_self_initiated + ) + + +class IpfsHandler(AbstractResponseHandler): + """A class for handling IPFS messages.""" + + SUPPORTED_PROTOCOL: Optional[PublicId] = IpfsMessage.protocol_id + allowed_response_performatives = frozenset( + { + IpfsMessage.Performative.IPFS_HASH, + IpfsMessage.Performative.FILES, + IpfsMessage.Performative.ERROR, + } + ) diff --git a/packages/valory/skills/abstract_round_abci/io_/__init__.py b/packages/valory/skills/abstract_round_abci/io_/__init__.py new file mode 100644 index 0000000..346ca66 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/io_/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains all the input-output operations logic of the behaviours.""" # pragma: nocover diff --git a/packages/valory/skills/abstract_round_abci/io_/ipfs.py b/packages/valory/skills/abstract_round_abci/io_/ipfs.py new file mode 100644 index 0000000..cfd914c --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/io_/ipfs.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains all the interaction operations of the behaviours with IPFS.""" + + +import os +from typing import Any, Dict, Optional, Type + +from packages.valory.skills.abstract_round_abci.io_.load import ( + CustomLoaderType, + Loader, + SupportedFiletype, + SupportedObjectType, +) +from packages.valory.skills.abstract_round_abci.io_.store import ( + CustomStorerType, + Storer, +) + + +class IPFSInteractionError(Exception): + """A custom exception for IPFS interaction errors.""" + + +class IPFSInteract: + """Class for interacting with IPFS.""" + + def __init__(self, loader_cls: Type = Loader, storer_cls: Type = Storer): + """Initialize an `IPFSInteract` object.""" + # Set loader/storer class. + self._loader_cls = loader_cls + self._storer_cls = storer_cls + + def store( + self, + filepath: str, + obj: SupportedObjectType, + multiple: bool, + filetype: Optional[SupportedFiletype] = None, + custom_storer: Optional[CustomStorerType] = None, + **kwargs: Any, + ) -> Dict[str, str]: + """Temporarily store a file locally, in order to send it to IPFS and retrieve a hash, and then delete it.""" + filepath = os.path.normpath(filepath) + if multiple: + # Add trailing slash in order to treat path as a folder. + filepath = os.path.join(filepath, "") + storer = self._storer_cls(filetype, custom_storer, filepath) + + try: + name_to_obj = storer.store(obj, multiple, **kwargs) + return name_to_obj + except Exception as e: # pylint: disable=broad-except + raise IPFSInteractionError(str(e)) from e + + def load( # pylint: disable=too-many-arguments + self, + serialized_objects: Dict[str, str], + filetype: Optional[SupportedFiletype] = None, + custom_loader: CustomLoaderType = None, + ) -> SupportedObjectType: + """Deserialize objects received via IPFS.""" + loader = self._loader_cls(filetype, custom_loader) + try: + deserialized_objects = loader.load(serialized_objects) + return deserialized_objects + except Exception as e: # pylint: disable=broad-except + raise IPFSInteractionError(str(e)) from e diff --git a/packages/valory/skills/abstract_round_abci/io_/load.py b/packages/valory/skills/abstract_round_abci/io_/load.py new file mode 100644 index 0000000..c9ceafb --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/io_/load.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains all the loading operations of the behaviours.""" + +import json +from abc import ABC, abstractmethod +from typing import Any, Callable, Dict, Optional + +from packages.valory.skills.abstract_round_abci.io_.store import ( + CustomObjectType, + NativelySupportedSingleObjectType, + SupportedFiletype, + SupportedObjectType, + SupportedSingleObjectType, +) + + +CustomLoaderType = Optional[Callable[[str], CustomObjectType]] +SupportedLoaderType = Callable[[str], SupportedSingleObjectType] + + +class AbstractLoader(ABC): + """An abstract `Loader` class.""" + + @abstractmethod + def load_single_object( + self, serialized_object: str + ) -> NativelySupportedSingleObjectType: + """Load a single object.""" + + def load(self, serialized_objects: Dict[str, str]) -> SupportedObjectType: + """ + Load one or more serialized objects. + + :param serialized_objects: A mapping of filenames to serialized object they contained. + :return: the loaded file(s). + """ + if len(serialized_objects) == 0: + # no objects are present, raise an error + raise ValueError('"serialized_objects" does not contain any objects') + + objects = {} + for filename, body in serialized_objects.items(): + objects[filename] = self.load_single_object(body) + + if len(objects) > 1: + # multiple object are present + # we return them as mapping of + # names and their value + return objects + + # one object is present, we simply return it as an object, i.e. without its name + _name, deserialized_body = objects.popitem() + return deserialized_body + + +class JSONLoader(AbstractLoader): + """A JSON file loader.""" + + def load_single_object( + self, serialized_object: str + ) -> NativelySupportedSingleObjectType: + """Read a json file. + + :param serialized_object: the file serialized into a JSON string. + :return: the deserialized json file's content. + """ + try: + deserialized_file = json.loads(serialized_object) + return deserialized_file + except json.JSONDecodeError as e: # pragma: no cover + raise IOError( + f"File '{serialized_object}' has an invalid JSON encoding!" + ) from e + except ValueError as e: # pragma: no cover + raise IOError( + f"There is an encoding error in the '{serialized_object}' file!" + ) from e + + +class Loader(AbstractLoader): + """Class which loads objects.""" + + def __init__(self, filetype: Optional[Any], custom_loader: CustomLoaderType): + """Initialize a `Loader`.""" + self._filetype = filetype + self._custom_loader = custom_loader + self.__filetype_to_loader: Dict[SupportedFiletype, SupportedLoaderType] = { + SupportedFiletype.JSON: JSONLoader().load_single_object, + } + + def load_single_object(self, serialized_object: str) -> SupportedSingleObjectType: + """Load a single file.""" + loader = self._get_single_loader_from_filetype() + return loader(serialized_object) + + def _get_single_loader_from_filetype(self) -> SupportedLoaderType: + """Get an object loader from a given filetype or keep a custom loader.""" + if self._filetype is not None: + return self.__filetype_to_loader[self._filetype] + + if self._custom_loader is not None: # pragma: no cover + return self._custom_loader + + raise ValueError( # pragma: no cover + "Please provide either a supported filetype or a custom loader function." + ) diff --git a/packages/valory/skills/abstract_round_abci/io_/paths.py b/packages/valory/skills/abstract_round_abci/io_/paths.py new file mode 100644 index 0000000..40b8923 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/io_/paths.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains all the path related operations of the behaviours.""" + + +import os + + +def create_pathdirs(path: str) -> None: + """Create the non-existing directories of a given path. + + :param path: the given path. + """ + dirname = os.path.dirname(path) + + if dirname: + os.makedirs(dirname, exist_ok=True) diff --git a/packages/valory/skills/abstract_round_abci/io_/store.py b/packages/valory/skills/abstract_round_abci/io_/store.py new file mode 100644 index 0000000..588dc36 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/io_/store.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains all the storing operations of the behaviours.""" + + +import json +import os.path +from abc import ABC, abstractmethod +from enum import Enum, auto +from typing import Any, Callable, Dict, Optional, TypeVar, Union, cast + +from packages.valory.skills.abstract_round_abci.io_.paths import create_pathdirs + + +StoredJSONType = Union[dict, list] +NativelySupportedSingleObjectType = StoredJSONType +NativelySupportedMultipleObjectsType = Dict[str, NativelySupportedSingleObjectType] +NativelySupportedObjectType = Union[ + NativelySupportedSingleObjectType, NativelySupportedMultipleObjectsType +] +NativelySupportedStorerType = Callable[[str, NativelySupportedObjectType, Any], None] +CustomObjectType = TypeVar("CustomObjectType") +CustomStorerType = Callable[[str, CustomObjectType, Any], None] +SupportedSingleObjectType = Union[NativelySupportedObjectType, CustomObjectType] +SupportedMultipleObjectsType = Dict[str, SupportedSingleObjectType] +SupportedObjectType = Union[SupportedSingleObjectType, SupportedMultipleObjectsType] +SupportedStorerType = Union[NativelySupportedStorerType, CustomStorerType] +NativelySupportedJSONStorerType = Callable[ + [str, Union[StoredJSONType, Dict[str, StoredJSONType]], Any], None +] + + +class SupportedFiletype(Enum): + """Enum for the supported filetypes of the IPFS interacting methods.""" + + JSON = auto() + + +class AbstractStorer(ABC): + """An abstract `Storer` class.""" + + def __init__(self, path: str): + """Initialize an abstract storer.""" + self._path = path + # Create the dirs of the path if it does not exist. + create_pathdirs(path) + + @abstractmethod + def serialize_object( + self, filename: str, obj: SupportedSingleObjectType, **kwargs: Any + ) -> Dict[str, str]: + """Store a single file.""" + + def store( + self, obj: SupportedObjectType, multiple: bool, **kwargs: Any + ) -> Dict[str, str]: + """Serialize one or multiple objects.""" + serialized_files: Dict[str, str] = {} + if multiple: + if not isinstance(obj, dict): # pragma: no cover + raise ValueError( + f"Cannot store multiple files of type {type(obj)}!" + f"Should be a dictionary of filenames mapped to their objects." + ) + for filename, single_obj in obj.items(): + filename = os.path.join(self._path, filename) + serialized_file = self.serialize_object(filename, single_obj, **kwargs) + serialized_files.update(**serialized_file) + else: + serialized_file = self.serialize_object(self._path, obj, **kwargs) + serialized_files.update(**serialized_file) + return serialized_files + + +class JSONStorer(AbstractStorer): + """A JSON file storer.""" + + def serialize_object( + self, filename: str, obj: NativelySupportedSingleObjectType, **kwargs: Any + ) -> Dict[str, str]: + """ + Serialize an object to JSON. + + :param filename: under which name the provided object should be serialized. Note that it will appear in IPFS with this name. + :param obj: the object to store. + :returns: a dict mapping the name to the serialized object. + """ + if not any(isinstance(obj, type_) for type_ in (dict, list)): + raise ValueError( # pragma: no cover + f"`JSONStorer` cannot be used with a {type(obj)}! Only with a {StoredJSONType}" + ) + try: + serialized_object = json.dumps(obj, ensure_ascii=False, indent=4) + name_to_obj = {filename: serialized_object} + return name_to_obj + except (TypeError, OSError) as e: # pragma: no cover + raise IOError(str(e)) from e + + +class Storer(AbstractStorer): + """Class which serializes objects.""" + + def __init__( + self, + filetype: Optional[Any], + custom_storer: Optional[CustomStorerType], + path: str, + ): + """Initialize a `Storer`.""" + super().__init__(path) + self._filetype = filetype + self._custom_storer = custom_storer + self._filetype_to_storer: Dict[Enum, SupportedStorerType] = { + SupportedFiletype.JSON: cast( + NativelySupportedJSONStorerType, JSONStorer(path).serialize_object + ), + } + + def serialize_object( + self, filename: str, obj: NativelySupportedObjectType, **kwargs: Any + ) -> Dict[str, str]: + """Store a single object.""" + storer = self._get_single_storer_from_filetype() + return storer(filename, obj, **kwargs) # type: ignore + + def _get_single_storer_from_filetype(self) -> SupportedStorerType: + """Get an object storer from a given filetype or keep a custom storer.""" + if self._filetype is not None: + return self._filetype_to_storer[self._filetype] + + if self._custom_storer is not None: # pragma: no cover + return self._custom_storer + + raise ValueError( # pragma: no cover + "Please provide either a supported filetype or a custom storing function." + ) diff --git a/packages/valory/skills/abstract_round_abci/models.py b/packages/valory/skills/abstract_round_abci/models.py new file mode 100644 index 0000000..27304eb --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/models.py @@ -0,0 +1,893 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the core models for all the ABCI apps.""" + +import inspect +import json +from abc import ABC, ABCMeta +from collections import Counter +from dataclasses import dataclass +from enum import Enum +from pathlib import Path +from time import time +from typing import ( + Any, + Callable, + Dict, + List, + Optional, + OrderedDict, + Tuple, + Type, + cast, + get_type_hints, +) + +from aea.configurations.data_types import PublicId +from aea.exceptions import enforce +from aea.skills.base import Model, SkillContext + +from packages.valory.protocols.http.message import HttpMessage +from packages.valory.skills.abstract_round_abci.base import ( + AbciApp, + AbciAppDB, + BaseSynchronizedData, + OffenceStatus, + ROUND_COUNT_DEFAULT, + RoundSequence, + VALUE_NOT_PROVIDED, + get_name, +) +from packages.valory.skills.abstract_round_abci.utils import ( + check, + check_type, + consensus_threshold, + get_data_from_nested_dict, + get_value_with_type, +) + + +MIN_RESET_PAUSE_DURATION = 10 +NUMBER_OF_RETRIES: int = 5 +DEFAULT_BACKOFF_FACTOR: float = 2.0 +DEFAULT_TYPE_NAME: str = "str" +DEFAULT_CHAIN = "ethereum" + + +class FrozenMixin: # pylint: disable=too-few-public-methods + """Mixin for classes to enforce read-only attributes.""" + + _frozen: bool = False + + def __delattr__(self, *args: Any) -> None: + """Override __delattr__ to make object immutable.""" + if self._frozen: + raise AttributeError( + "This object is frozen! To unfreeze switch `self._frozen` via `__dict__`." + ) + super().__delattr__(*args) + + def __setattr__(self, *args: Any) -> None: + """Override __setattr__ to make object immutable.""" + if self._frozen: + raise AttributeError( + "This object is frozen! To unfreeze switch `self._frozen` via `__dict__`." + ) + super().__setattr__(*args) + + +class TypeCheckMixin: # pylint: disable=too-few-public-methods + """Mixin for data classes & models to enforce attribute types on construction.""" + + def __post_init__(self) -> None: + """Check that the type of the provided attributes is correct.""" + for attr, type_ in get_type_hints(self).items(): + value = getattr(self, attr) + check_type(attr, value, type_) + + @classmethod + def _ensure(cls, key: str, kwargs: Dict, type_: Any) -> Any: + """Get and ensure the configuration field is not None (if no default is provided) and of correct type.""" + enforce("skill_context" in kwargs, "Only use on models!") + skill_id = kwargs["skill_context"].skill_id + enforce( + key in kwargs, + f"'{key}' of type '{type_}' required, but it is not set in `models.params.args` of `skill.yaml` of `{skill_id}`", + ) + value = kwargs.pop(key) + try: + check_type(key, value, type_) + except TypeError: # pragma: nocover + enforce( + False, + f"'{key}' must be a {type_}, but type {type(value)} was found in `models.params.args` of `skill.yaml` of `{skill_id}`", + ) + return value + + +@dataclass(frozen=True) +class GenesisBlock(TypeCheckMixin): + """A dataclass to store the genesis block.""" + + max_bytes: str + max_gas: str + time_iota_ms: str + + def to_json(self) -> Dict[str, str]: + """Get a GenesisBlock instance as a json dictionary.""" + return { + "max_bytes": self.max_bytes, + "max_gas": self.max_gas, + "time_iota_ms": self.time_iota_ms, + } + + +@dataclass(frozen=True) +class GenesisEvidence(TypeCheckMixin): + """A dataclass to store the genesis evidence.""" + + max_age_num_blocks: str + max_age_duration: str + max_bytes: str + + def to_json(self) -> Dict[str, str]: + """Get a GenesisEvidence instance as a json dictionary.""" + return { + "max_age_num_blocks": self.max_age_num_blocks, + "max_age_duration": self.max_age_duration, + "max_bytes": self.max_bytes, + } + + +@dataclass(frozen=True) +class GenesisValidator(TypeCheckMixin): + """A dataclass to store the genesis validator.""" + + pub_key_types: Tuple[str, ...] + + def to_json(self) -> Dict[str, List[str]]: + """Get a GenesisValidator instance as a json dictionary.""" + return {"pub_key_types": list(self.pub_key_types)} + + +@dataclass(frozen=True) +class GenesisConsensusParams(TypeCheckMixin): + """A dataclass to store the genesis consensus parameters.""" + + block: GenesisBlock + evidence: GenesisEvidence + validator: GenesisValidator + version: dict + + @classmethod + def from_json_dict(cls, json_dict: dict) -> "GenesisConsensusParams": + """Get a GenesisConsensusParams instance from a json dictionary.""" + block = GenesisBlock(**json_dict["block"]) + evidence = GenesisEvidence(**json_dict["evidence"]) + validator = GenesisValidator(tuple(json_dict["validator"]["pub_key_types"])) + return cls(block, evidence, validator, json_dict["version"]) + + def to_json(self) -> Dict[str, Any]: + """Get a GenesisConsensusParams instance as a json dictionary.""" + return { + "block": self.block.to_json(), + "evidence": self.evidence.to_json(), + "validator": self.validator.to_json(), + "version": self.version, + } + + +@dataclass(frozen=True) +class GenesisConfig(TypeCheckMixin): + """A dataclass to store the genesis configuration.""" + + genesis_time: str + chain_id: str + consensus_params: GenesisConsensusParams + voting_power: str + + @classmethod + def from_json_dict(cls, json_dict: dict) -> "GenesisConfig": + """Get a GenesisConfig instance from a json dictionary.""" + consensus_params = GenesisConsensusParams.from_json_dict( + json_dict["consensus_params"] + ) + return cls( + json_dict["genesis_time"], + json_dict["chain_id"], + consensus_params, + json_dict["voting_power"], + ) + + def to_json(self) -> Dict[str, Any]: + """Get a GenesisConfig instance as a json dictionary.""" + return { + "genesis_time": self.genesis_time, + "chain_id": self.chain_id, + "consensus_params": self.consensus_params.to_json(), + "voting_power": self.voting_power, + } + + +class BaseParams( + Model, FrozenMixin, TypeCheckMixin +): # pylint: disable=too-many-instance-attributes + """Parameters.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """ + Initialize the parameters object. + + The genesis configuration should be a dictionary with the following format: + genesis_time: str + chain_id: str + consensus_params: + block: + max_bytes: str + max_gas: str + time_iota_ms: str + evidence: + max_age_num_blocks: str + max_age_duration: str + max_bytes: str + validator: + pub_key_types: List[str] + version: dict + voting_power: str + + :param args: positional arguments + :param kwargs: keyword arguments + """ + self.genesis_config: GenesisConfig = GenesisConfig.from_json_dict( + self._ensure("genesis_config", kwargs, dict) + ) + self.service_id: str = self._ensure("service_id", kwargs, str) + self.tendermint_url: str = self._ensure("tendermint_url", kwargs, str) + self.max_healthcheck: int = self._ensure("max_healthcheck", kwargs, int) + self.round_timeout_seconds: float = self._ensure( + "round_timeout_seconds", kwargs, float + ) + self.sleep_time: int = self._ensure("sleep_time", kwargs, int) + self.retry_timeout: int = self._ensure("retry_timeout", kwargs, int) + self.retry_attempts: int = self._ensure("retry_attempts", kwargs, int) + self.keeper_timeout: float = self._ensure("keeper_timeout", kwargs, float) + self.reset_pause_duration: int = self._ensure_gte( + "reset_pause_duration", kwargs, int, min_value=MIN_RESET_PAUSE_DURATION + ) + self.drand_public_key: str = self._ensure("drand_public_key", kwargs, str) + self.tendermint_com_url: str = self._ensure("tendermint_com_url", kwargs, str) + self.tendermint_max_retries: int = self._ensure( + "tendermint_max_retries", kwargs, int + ) + self.tendermint_check_sleep_delay: int = self._ensure( + "tendermint_check_sleep_delay", kwargs, int + ) + self.reset_tendermint_after: int = self._ensure( + "reset_tendermint_after", kwargs, int + ) + self.cleanup_history_depth: int = self._ensure( + "cleanup_history_depth", kwargs, int + ) + self.cleanup_history_depth_current: Optional[int] = self._ensure( + "cleanup_history_depth_current", kwargs, Optional[int] + ) + self.request_timeout: float = self._ensure("request_timeout", kwargs, float) + self.request_retry_delay: float = self._ensure( + "request_retry_delay", kwargs, float + ) + self.tx_timeout: float = self._ensure("tx_timeout", kwargs, float) + self.max_attempts: int = self._ensure("max_attempts", kwargs, int) + self.service_registry_address: Optional[str] = self._ensure( + "service_registry_address", kwargs, Optional[str] + ) + self.on_chain_service_id: Optional[int] = self._ensure( + "on_chain_service_id", kwargs, Optional[int] + ) + self.share_tm_config_on_startup: bool = self._ensure( + "share_tm_config_on_startup", kwargs, bool + ) + self.tendermint_p2p_url: str = self._ensure("tendermint_p2p_url", kwargs, str) + self.use_termination: bool = self._ensure("use_termination", kwargs, bool) + self.use_slashing: bool = self._ensure("use_slashing", kwargs, bool) + self.slash_cooldown_hours: int = self._ensure( + "slash_cooldown_hours", kwargs, int + ) + self.slash_threshold_amount: int = self._ensure( + "slash_threshold_amount", kwargs, int + ) + self.light_slash_unit_amount: int = self._ensure( + "light_slash_unit_amount", kwargs, int + ) + self.serious_slash_unit_amount: int = self._ensure( + "serious_slash_unit_amount", kwargs, int + ) + self.setup_params: Dict[str, Any] = self._ensure("setup", kwargs, dict) + # TODO add to all configs + self.default_chain_id: str = kwargs.get("default_chain_id", DEFAULT_CHAIN) + + # we sanitize for null values as these are just kept for schema definitions + skill_id = kwargs["skill_context"].skill_id + super().__init__(*args, **kwargs) + + if not self.context.is_abstract_component: + # setup data are mandatory for non-abstract skills, + # and they should always contain at least `all_participants` and `safe_contract_address` + self._ensure_setup( + { + get_name(BaseSynchronizedData.safe_contract_address): str, + get_name(BaseSynchronizedData.all_participants): List[str], + get_name(BaseSynchronizedData.consensus_threshold): cast( + Type, Optional[int] + ), + }, + skill_id, + ) + self._frozen = True + + def _ensure_setup( + self, necessary_params: Dict[str, Type], skill_id: PublicId + ) -> Any: + """Ensure that the `setup` params contain all the `necessary_keys` and have the correct types.""" + enforce(bool(self.setup_params), "`setup` params contain no values!") + + for key, type_ in necessary_params.items(): + # check that the key is present, note that None is acceptable for optional keys + value = self.setup_params.get(key, VALUE_NOT_PROVIDED) + if value is VALUE_NOT_PROVIDED: + fail_msg = f"Value for `{key}` missing from the `setup` params." + enforce(False, fail_msg) + + # check that the value is of the correct type + try: + check_type(key, value, type_) + except TypeError: # pragma: nocover + enforce( + False, + f"'{key}' must be a {type_}, but type {type(value)} was found in `models.params.args.setup` " + f"of `skill.yaml` of `{skill_id}`", + ) + + def _ensure_gte( + self, key: str, kwargs: Dict[str, Any], type_: Type, min_value: Any + ) -> Any: + """Ensure that the value for the key is greater than or equal to the provided min_value.""" + err = check(min_value, type_) + enforce( + err is None, + f"min_value must be of type {type_.__name__}, but got {type(min_value).__name__}.", + ) + value = self._ensure(key, kwargs, type_) + enforce( + value >= min_value, f"`{key}` must be greater than or equal to {min_value}." + ) + return value + + +class _MetaSharedState(ABCMeta): + """A metaclass that validates SharedState's attributes.""" + + def __new__(mcs, name: str, bases: Tuple, namespace: Dict, **kwargs: Any) -> Type: # type: ignore + """Initialize the class.""" + new_cls = super().__new__(mcs, name, bases, namespace, **kwargs) + + if ABC in bases: + # abstract class, return + return new_cls + if not issubclass(new_cls, SharedState): + # the check only applies to SharedState subclasses + return new_cls + + mcs._check_consistency(cast(Type[SharedState], new_cls)) + return new_cls + + @classmethod + def _check_consistency(mcs, shared_state_cls: Type["SharedState"]) -> None: + """Check consistency of class attributes.""" + mcs._check_required_class_attributes(shared_state_cls) + + @classmethod + def _check_required_class_attributes( + mcs, shared_state_cls: Type["SharedState"] + ) -> None: + """Check that required class attributes are set.""" + if not hasattr(shared_state_cls, "abci_app_cls"): + raise AttributeError(f"'abci_app_cls' not set on {shared_state_cls}") + abci_app_cls = shared_state_cls.abci_app_cls + if not inspect.isclass(abci_app_cls): + raise AttributeError(f"The object `{abci_app_cls}` is not a class") + if not issubclass(abci_app_cls, AbciApp): + cls_name = AbciApp.__name__ + cls_module = AbciApp.__module__ + raise AttributeError( + f"The class {abci_app_cls} is not an instance of {cls_module}.{cls_name}" + ) + + +class SharedState(Model, ABC, metaclass=_MetaSharedState): # type: ignore + """Keep the current shared state of the skill.""" + + abci_app_cls: Type[AbciApp] + + def __init__( + self, + *args: Any, + skill_context: SkillContext, + **kwargs: Any, + ) -> None: + """Initialize the state.""" + self.abci_app_cls._is_abstract = skill_context.is_abstract_component + self._round_sequence: Optional[RoundSequence] = None + # a mapping of the agents' addresses to their initial Tendermint configuration, to be retrieved via ACN + self.initial_tm_configs: Dict[str, Optional[Dict[str, Any]]] = {} + # a mapping of the other agents' addresses to ACN deliverables + self.address_to_acn_deliverable: Dict[str, Any] = {} + self.tm_recovery_params: TendermintRecoveryParams = TendermintRecoveryParams( + self.abci_app_cls.initial_round_cls.auto_round_id() + ) + kwargs["skill_context"] = skill_context + super().__init__(*args, **kwargs) + + def setup_slashing(self, validator_to_agent: Dict[str, str]) -> None: + """Initialize the structures required for slashing.""" + configured_agents = set(self.initial_tm_configs.keys()) + agents_mapped = set(validator_to_agent.values()) + diff = agents_mapped.symmetric_difference(configured_agents) + if diff: + raise ValueError( + f"Trying to use the mapping `{validator_to_agent}`, which contains validators for non-configured " + "agents and/or does not contain validators for some configured agents. " + f"The agents which have been configured via ACN are `{configured_agents}` and the diff was for {diff}." + ) + self.round_sequence.validator_to_agent = validator_to_agent + self.round_sequence.offence_status = { + agent: OffenceStatus() for agent in agents_mapped + } + + def get_validator_address(self, agent_address: str) -> str: + """Get the validator address of an agent.""" + if agent_address not in self.synchronized_data.all_participants: + raise ValueError( + f"The validator address of non-participating agent `{agent_address}` was requested." + ) + + try: + agent_config = self.initial_tm_configs[agent_address] + except KeyError as e: + raise ValueError( + "SharedState's setup was not performed successfully." + ) from e + + if agent_config is None: + raise ValueError( + f"ACN registration has not been successfully performed for agent `{agent_address}`. " + "Have you set the `share_tm_config_on_startup` flag to `true` in the configuration?" + ) + + validator_address = agent_config.get("address", None) + if validator_address is None: + raise ValueError( + f"The tendermint configuration for agent `{agent_address}` is invalid: `{agent_config}`." + ) + + return validator_address + + def acn_container(self) -> Dict[str, Any]: + """Create a container for ACN results, i.e., a mapping from others' addresses to `None`.""" + ourself = {self.context.agent_address} + others_addresses = self.synchronized_data.all_participants - ourself + + return dict.fromkeys(others_addresses) + + def setup(self) -> None: + """Set up the model.""" + self._round_sequence = RoundSequence(self.context, self.abci_app_cls) + setup_params = cast(BaseParams, self.context.params).setup_params + self.round_sequence.setup( + BaseSynchronizedData( + AbciAppDB( + setup_data=AbciAppDB.data_to_lists(setup_params), + cross_period_persisted_keys=self.abci_app_cls.cross_period_persisted_keys, + logger=self.context.logger, + ) + ), + self.context.logger, + ) + if not self.context.is_abstract_component: + self.initial_tm_configs = dict.fromkeys( + self.synchronized_data.all_participants + ) + + @property + def round_sequence(self) -> RoundSequence: + """Get the round_sequence.""" + if self._round_sequence is None: + raise ValueError("round sequence not available") + return self._round_sequence + + @property + def synchronized_data(self) -> BaseSynchronizedData: + """Get the latest synchronized_data if available.""" + return self.round_sequence.latest_synchronized_data + + def get_acn_result(self) -> Any: + """Get the majority of the ACN deliverables.""" + if len(self.address_to_acn_deliverable) == 0: + return None + + # the current agent does not participate, so we need `nb_participants - 1` + threshold = consensus_threshold(self.synchronized_data.nb_participants - 1) + counter = Counter(self.address_to_acn_deliverable.values()) + most_common_value, n_appearances = counter.most_common(1)[0] + + if n_appearances < threshold: + return None + + self.context.logger.debug( + f"ACN result is '{most_common_value}' from '{self.address_to_acn_deliverable}'." + ) + return most_common_value + + +class Requests(Model, FrozenMixin): + """Keep the current pending requests.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize the state.""" + # mapping from dialogue reference nonce to a callback + self.request_id_to_callback: Dict[str, Callable] = {} + super().__init__(*args, **kwargs) + self._frozen = True + + +class UnexpectedResponseError(Exception): + """Exception class for unexpected responses from Apis.""" + + +@dataclass +class ResponseInfo(TypeCheckMixin): + """A dataclass to hold all the information related to the response.""" + + response_key: Optional[str] + response_index: Optional[int] + response_type: str + error_key: Optional[str] + error_index: Optional[int] + error_type: str + error_data: Any = None + + @classmethod + def from_json_dict(cls, kwargs: Dict) -> "ResponseInfo": + """Initialize a response info object from kwargs.""" + response_key: Optional[str] = kwargs.pop("response_key", None) + response_index: Optional[int] = kwargs.pop("response_index", None) + response_type: str = kwargs.pop("response_type", DEFAULT_TYPE_NAME) + error_key: Optional[str] = kwargs.pop("error_key", None) + error_index: Optional[int] = kwargs.pop("error_index", None) + error_type: str = kwargs.pop("error_type", DEFAULT_TYPE_NAME) + return cls( + response_key, + response_index, + response_type, + error_key, + error_index, + error_type, + ) + + +@dataclass +class RetriesInfo(TypeCheckMixin): + """A dataclass to hold all the information related to the retries.""" + + retries: int + backoff_factor: float + retries_attempted: int = 0 + + @classmethod + def from_json_dict(cls, kwargs: Dict) -> "RetriesInfo": + """Initialize a retries info object from kwargs.""" + retries: int = kwargs.pop("retries", NUMBER_OF_RETRIES) + backoff_factor: float = kwargs.pop("backoff_factor", DEFAULT_BACKOFF_FACTOR) + return cls(retries, backoff_factor) + + @property + def suggested_sleep_time(self) -> float: + """The suggested amount of time to sleep.""" + return self.backoff_factor**self.retries_attempted + + +@dataclass(frozen=True) +class TendermintRecoveryParams(TypeCheckMixin): + """ + A dataclass to hold all parameters related to agent <-> tendermint recovery procedures. + + This must be frozen so that we make sure it does not get edited. + """ + + reset_from_round: str + round_count: int = ROUND_COUNT_DEFAULT + reset_params: Optional[Dict[str, str]] = None + serialized_db_state: Optional[str] = None + + def __hash__(self) -> int: + """Hash the object.""" + return hash( + self.reset_from_round + + str(self.round_count) + + str(self.serialized_db_state) + + json.dumps(self.reset_params, sort_keys=True) + ) + + +class ApiSpecs(Model, FrozenMixin, TypeCheckMixin): + """A model that wraps APIs to get cryptocurrency prices.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize ApiSpecsModel.""" + self.url: str = self._ensure("url", kwargs, str) + self.api_id: str = self._ensure("api_id", kwargs, str) + self.method: str = self._ensure("method", kwargs, str) + self.headers: Dict[str, str] = dict( + self._ensure("headers", kwargs, OrderedDict[str, str]) + ) + self.parameters: Dict[str, str] = dict( + self._ensure("parameters", kwargs, OrderedDict[str, str]) + ) + self.response_info = ResponseInfo.from_json_dict(kwargs) + self.retries_info = RetriesInfo.from_json_dict(kwargs) + super().__init__(*args, **kwargs) + self._frozen = True + + def get_spec( + self, + ) -> Dict: + """Returns dictionary containing api specifications.""" + + return { + "url": self.url, + "method": self.method, + "headers": self.headers, + "parameters": self.parameters, + } + + def _log_response(self, decoded_response: str) -> None: + """Log the decoded response message using error level.""" + pretty_json_str = json.dumps(decoded_response, indent=4) + self.context.logger.error(f"Response: {pretty_json_str}") + + @staticmethod + def _parse_response( + response_data: Any, + response_keys: Optional[str], + response_index: Optional[int], + response_type: str, + ) -> Any: + """Parse a response from an API.""" + if response_keys is not None: + response_data = get_data_from_nested_dict(response_data, response_keys) + + if response_index is not None: + response_data = response_data[response_index] + + return get_value_with_type(response_data, response_type) + + def _get_error_from_response(self, response_data: Any) -> Any: + """Try to get an error from the response.""" + try: + return self._parse_response( + response_data, + self.response_info.error_key, + self.response_info.error_index, + self.response_info.error_type, + ) + except (KeyError, IndexError, TypeError): + self.context.logger.error( + f"Could not parse error using the given key(s) ({self.response_info.error_key}) " + f"and index ({self.response_info.error_index})!" + ) + return None + + def _parse_response_data(self, response_data: Any) -> Any: + """Get the response data.""" + try: + return self._parse_response( + response_data, + self.response_info.response_key, + self.response_info.response_index, + self.response_info.response_type, + ) + except (KeyError, IndexError, TypeError) as e: + raise UnexpectedResponseError from e + + def process_response(self, response: HttpMessage) -> Any: + """Process response from api.""" + decoded_response = response.body.decode() + self.response_info.error_data = None + + try: + response_data = json.loads(decoded_response) + except json.JSONDecodeError: + self.context.logger.error("Could not parse the response body!") + self._log_response(decoded_response) + return None + + try: + return self._parse_response_data(response_data) + except UnexpectedResponseError: + self.context.logger.error( + f"Could not access response using the given key(s) ({self.response_info.response_key}) " + f"and index ({self.response_info.response_index})!" + ) + self._log_response(decoded_response) + self.response_info.error_data = self._get_error_from_response(response_data) + return None + + def increment_retries(self) -> None: + """Increment the retries counter.""" + self.retries_info.retries_attempted += 1 + + def reset_retries(self) -> None: + """Reset the retries counter.""" + self.retries_info.retries_attempted = 0 + + def is_retries_exceeded(self) -> bool: + """Check if the retries amount has been exceeded.""" + return self.retries_info.retries_attempted > self.retries_info.retries + + +class BenchmarkBlockTypes(Enum): + """Benchmark block types.""" + + LOCAL = "local" + CONSENSUS = "consensus" + TOTAL = "total" + + +class BenchmarkBlock: + """ + Benchmark + + This class represents logic to measure the code block using a + context manager. + """ + + start: float + total_time: float + block_type: str + + def __init__(self, block_type: str) -> None: + """Benchmark for single round.""" + self.block_type = block_type + self.start = 0 + self.total_time = 0 + + def __enter__( + self, + ) -> None: + """Enter context.""" + self.start = time() + + def __exit__(self, *args: List, **kwargs: Dict) -> None: + """Exit context""" + self.total_time = time() - self.start + + +class BenchmarkBehaviour: + """ + BenchmarkBehaviour + + This class represents logic to benchmark a single behaviour. + """ + + local_data: Dict[str, BenchmarkBlock] + + def __init__( + self, + ) -> None: + """Initialize Benchmark behaviour object.""" + self.local_data = {} + + def _measure(self, block_type: str) -> BenchmarkBlock: + """ + Returns a BenchmarkBlock object. + + :param block_type: type of block (e.g. local, consensus, request) + :return: BenchmarkBlock + """ + + if block_type not in self.local_data: + self.local_data[block_type] = BenchmarkBlock(block_type) + + return self.local_data[block_type] + + def local( + self, + ) -> BenchmarkBlock: + """Measure local block.""" + return self._measure(BenchmarkBlockTypes.LOCAL.value) + + def consensus( + self, + ) -> BenchmarkBlock: + """Measure consensus block.""" + return self._measure(BenchmarkBlockTypes.CONSENSUS.value) + + +class BenchmarkTool(Model, TypeCheckMixin, FrozenMixin): + """ + BenchmarkTool + + Tool to benchmark ABCI apps. + """ + + benchmark_data: Dict[str, BenchmarkBehaviour] + log_dir: Path + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Benchmark tool for rounds behaviours.""" + self.benchmark_data = {} + log_dir_ = self._ensure("log_dir", kwargs, str) + self.log_dir = Path(log_dir_) + super().__init__(*args, **kwargs) + self._frozen = True + + def measure(self, behaviour: str) -> BenchmarkBehaviour: + """Measure time to complete round.""" + if behaviour not in self.benchmark_data: + self.benchmark_data[behaviour] = BenchmarkBehaviour() + return self.benchmark_data[behaviour] + + @property + def data( + self, + ) -> List: + """Returns formatted data.""" + + behavioural_data = [] + for behaviour, tool in self.benchmark_data.items(): + data = {k: v.total_time for k, v in tool.local_data.items()} + data[BenchmarkBlockTypes.TOTAL.value] = sum(data.values()) + behavioural_data.append({"behaviour": behaviour, "data": data}) + + return behavioural_data + + def save(self, period: int = 0, reset: bool = True) -> None: + """Save logs to a file.""" + + try: + self.log_dir.mkdir(exist_ok=True) + agent_dir = self.log_dir / self.context.agent_address + agent_dir.mkdir(exist_ok=True) + filepath = agent_dir / f"{period}.json" + + with open(str(filepath), "w+", encoding="utf-8") as outfile: + json.dump(self.data, outfile) + self.context.logger.debug(f"Saving benchmarking data for period: {period}") + + except PermissionError as e: # pragma: nocover + self.context.logger.error(f"Error saving benchmark data:\n{e}") + + if reset: + self.reset() + + def reset( + self, + ) -> None: + """Reset benchmark data""" + self.benchmark_data.clear() diff --git a/packages/valory/skills/abstract_round_abci/skill.yaml b/packages/valory/skills/abstract_round_abci/skill.yaml new file mode 100644 index 0000000..67054f0 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/skill.yaml @@ -0,0 +1,164 @@ +name: abstract_round_abci +author: valory +version: 0.1.0 +type: skill +description: abstract round-based ABCI application +license: Apache-2.0 +aea_version: '>=1.0.0, <2.0.0' +fingerprint: + README.md: bafybeievb7bhfm46p5adx3x4gvsynjpq35fcrrapzn5m2whcdt4ufxfvfq + __init__.py: bafybeihxbinbrvhj2edqthpzc2mywfzxzkf7l4v5uj6ubwnffrwgzelmre + abci_app_chain.py: bafybeibhzrixbp5x26wqhb6ogtr3af5lc4tax7lcsvk4v5rvg4psrq5yzi + base.py: bafybeihm7lf4nfqcwfvs4antge2l7eyc7vrafaw6p5canlxwy4qy4akwme + behaviour_utils.py: bafybeidhnu2ucjhlluwthpl4d6374nzmvjopy7byc2uyirajb3kswfggle + behaviours.py: bafybeifzbzy2ppabm6dpgvbsuvpgduyg7g7rei6u4ouy3nnak5top5be5u + common.py: bafybeib4coyhaxvpup7m25lsab2lpebv2wrkjp2cwihuitxmaibo6u6z2m + dialogues.py: bafybeid5sgrfa7ghnnjpssltgtey5gzt5kc2jlaitffaukvhhdbhrzcjti + handlers.py: bafybeidgby4h72qgcp3civ3c55oz3k7s4gdbkcotwhqjsbft6ylbaenjxy + io_/__init__.py: bafybeihv6ytxeo5jkbdlqjum4pfo4aaluvw4m7c55k5xncvvs7ubrlokhy + io_/ipfs.py: bafybeiffdxdt36rcwu5tyfav2umvw3hvlfjwbys3626p2g2gdlfi7djzly + io_/load.py: bafybeigkywwlsheqvd4gpyfwaxqzkkb2ih2poyicqk7e7n2mrsghxzyns4 + io_/paths.py: bafybeicfno2l4vwtmjcm3rzpp6tqi3xlkof47pypf5teecad22d44u2ple + io_/store.py: bafybeig24lslvhf7amim55ig5zzre4z45pcx3r2ozlagg3mtbr6rry2wpu + models.py: bafybeiaffpzuduwwo367cqm4uzl46mq34pdspq57o5itdb5ivyi4s743by + test_tools/__init__.py: bafybeibayeahoo73eztt2chpwi45taj2uv3dxbpyn47ksqfjoepjyaoca4 + test_tools/abci_app.py: bafybeigmrjzxfoc63xgecyngdecz4msvze4aw2iejcjewatjefjbvdlmce + test_tools/base.py: bafybeibef4lclyecne5qj4zaxnaxaqzwpxjaitqqmddgsiezduhb7pfxly + test_tools/common.py: bafybeibxlx7es632kdoeivfrjahns3kknkxfmw4rj2dcxjwqm5j6vx25sq + test_tools/integration.py: bafybeifqq3bx46hz2deph3usvrt7u45tpsapvocofd2zu3yh7rfl5nlmzq + test_tools/rounds.py: bafybeie576yxtiramzt5czpt4hnv76gfetzio2t3k5kprhdhvbpfddbaem + tests/__init__.py: bafybeie54sgqid64dyarbcttz3nnmyympyrtdyxy4lcc7c7yjxhefodbgq + tests/conftest.py: bafybeiauvmnuetxooprdsy3vlys3pha6x2rfg7acr3xrdfffr7onlmnave + tests/data/__init__.py: bafybeifmqjnrqgbau4tshhdtrosru7xyjky72ljlrf3ynrk76fxjcsgfpi + tests/data/dummy_abci/__init__.py: bafybeiaoqyjlgez5gkvutl22ihebcjk3zskve5gdt5wbap5zkmhehoddca + tests/data/dummy_abci/behaviours.py: bafybeibei4ngebbktuq6a2uvwhrulgkvn6uhaj5k3a75zihkxwnfarqh4m + tests/data/dummy_abci/dialogues.py: bafybeiaswubmqa7trhajbjn34okmpftk2sehsqrjg7znzrrd7j32xzx4vq + tests/data/dummy_abci/handlers.py: bafybeifik3ftljs63u7nm4gadxpqbcvqj53p7qftzzzfto3ioad57k3x3u + tests/data/dummy_abci/models.py: bafybeiear3i45wbaylrkbnm2fbtqorxx56glul36piuah7m7jb56f5rpoq + tests/data/dummy_abci/payloads.py: bafybeiczldqiumb7prcusb7l5vb575vschwyseyigpupvteldfyz7h6fyi + tests/data/dummy_abci/rounds.py: bafybeihhheznpcntg4z5cdd7dysnivo2g4x5biv7blriyiyoouqp6xf5aq + tests/test_abci_app_chain.py: bafybeihqvjkcwkwxowhb3umtk52us4pd5f6nbppw4ycx76oljw4j3j7xpa + tests/test_base.py: bafybeihtx2ktf6uck2l6yw72lvnvm5y224vlgawxette75cluc6juedeqe + tests/test_base_rounds.py: bafybeiadkpwuhz6y5k5ffvoqvyi6nqetf5ov5bmodejge7yvscm6yqzpse + tests/test_behaviours.py: bafybeibxxev34avddvezqumr56k7txmqkubl2c5u6y7ydjqn6kp3wabbvq + tests/test_behaviours_utils.py: bafybeidkxzhu26r2shkblz2l3syzc62uet4cxrdbschnf7vuwuuior6xkm + tests/test_common.py: bafybeiekicwjh3vu5kqppictya2bmqm3p5dcauj7cvsiunvhhultpzmyla + tests/test_dialogues.py: bafybeigpfrslqaz2yullyehia5bsl7cmy2qqxtz627ig7rbrypw5xfzeum + tests/test_handlers.py: bafybeih64lmsukci3oc5mwi636gntyx243xnbzwx64dwjxittch77qyqsu + tests/test_io/__init__.py: bafybeid3sssvbbyju4snrdssxyafleuo57sqyuepl25btxcbuj3p5oonsm + tests/test_io/test_ipfs.py: bafybeidm6f6naq6y7ntoivrqon2bkwdvd2dqru467fxqvgonv5oq5huhra + tests/test_io/test_load.py: bafybeidgnxt5rt67ackbcgi5vnlliedxakcnzgihogplolck7kp57pc6iy + tests/test_io/test_store.py: bafybeid2zbdjtgbplenacudk6re7si7dloqs2u7faqt7vhapjipjuw35ku + tests/test_models.py: bafybeicrbu6xtprfgwjs3msa3idilwqe3ymz5zx6xm326huhspzrdngrwi + tests/test_tools/__init__.py: bafybeiew6gu4pgp2sjevq4dbnmv2ail5dph7vj4yi7h3eae4gzx7vj7cbq + tests/test_tools/base.py: bafybeihi7ax53326dhin3riwwwk3bouqvsoeq26han4nspodzj6hrk3gia + tests/test_tools/test_base.py: bafybeie2hox7v6sy677grl6awq57ouliohpwhmlvrypz5rqcz5gxsxn24y + tests/test_tools/test_common.py: bafybeieauphpcqm5on7d2u2lc5lrf3esbhojp6sxlf7phrlmpqy5cfoitq + tests/test_tools/test_integration.py: bafybeidxkvb2kizi7djrpuw446dqxo2v5s7j2dbdrdpfmnd2ggezaxbnkm + tests/test_tools/test_rounds.py: bafybeibaoj4miysneipgukz7xufs47vpv5rds3ptgmu3yxlcl7gjss6ccm + tests/test_utils.py: bafybeift6igxoan2bnuexps7rrdl25jmlniqujw3odnir3cgjy4oukjjfq + utils.py: bafybeidbha3c3tcxo4lhucyx2x6yra4z2p2fp6sucqqzhxanbvgrraykbi +fingerprint_ignore_patterns: [] +connections: +- valory/abci:0.1.0:bafybeie4eixvrdpc5ifoovj24a6res6g2e22dl6di6gzib7d3fczshzyti +- valory/http_client:0.23.0:bafybeihi772xgzpqeipp3fhmvpct4y6e6tpjp4sogwqrnf3wqspgeilg4u +- valory/ipfs:0.1.0:bafybeiefkqvh5ylbk77xylcmshyuafmiecopt4gvardnubq52psvogis6a +- valory/ledger:0.19.0:bafybeihynkdraqthjtv74qk3nc5r2xubniqx2hhzpxn7bd4qmlf7q4wruq +- valory/p2p_libp2p_client:0.1.0:bafybeid3xg5k2ol5adflqloy75ibgljmol6xsvzvezebsg7oudxeeolz7e +contracts: +- valory/service_registry:0.1.0:bafybeieqgcuxmz4uxvlyb62mfsf33qy4xwa5lrij4vvcmrtcsfkng43oyq +protocols: +- open_aea/signing:1.0.0:bafybeihv62fim3wl2bayavfcg3u5e5cxu3b7brtu4cn5xoxd6lqwachasi +- valory/abci:0.1.0:bafybeiaqmp7kocbfdboksayeqhkbrynvlfzsx4uy4x6nohywnmaig4an7u +- valory/contract_api:1.0.0:bafybeidgu7o5llh26xp3u3ebq3yluull5lupiyeu6iooi2xyymdrgnzq5i +- valory/http:1.0.0:bafybeifugzl63kfdmwrxwphrnrhj7bn6iruxieme3a4ntzejf6kmtuwmae +- valory/ipfs:0.1.0:bafybeiftxi2qhreewgsc5wevogi7yc5g6hbcbo4uiuaibauhv3nhfcdtvm +- valory/ledger_api:1.0.0:bafybeihdk6psr4guxmbcrc26jr2cbgzpd5aljkqvpwo64bvaz7tdti2oni +- valory/tendermint:0.1.0:bafybeig4mi3vmlv5zpbjbfuzcgida6j5f2nhrpedxicmrrfjweqc5r7cra +skills: +- valory/abstract_abci:0.1.0:bafybeihu2bcgjk2tqjiq2zhk3uogtfszqn4osvdt7ho3fubdpdj4jgdfjm +behaviours: + main: + args: {} + class_name: AbstractRoundBehaviour +handlers: + abci: + args: {} + class_name: ABCIHandler + contract_api: + args: {} + class_name: ContractApiHandler + http: + args: {} + class_name: HttpHandler + ipfs: + args: {} + class_name: IpfsHandler + ledger_api: + args: {} + class_name: LedgerApiHandler + signing: + args: {} + class_name: SigningHandler + tendermint: + args: {} + class_name: TendermintHandler +models: + abci_dialogues: + args: {} + class_name: AbciDialogues + api_specs: + args: {} + class_name: ApiSpecs + benchmark_tool: + args: + log_dir: /logs + class_name: BenchmarkTool + contract_api_dialogues: + args: {} + class_name: ContractApiDialogues + http_dialogues: + args: {} + class_name: HttpDialogues + ipfs_dialogues: + args: {} + class_name: IpfsDialogues + ledger_api_dialogues: + args: {} + class_name: LedgerApiDialogues + requests: + args: {} + class_name: Requests + signing_dialogues: + args: {} + class_name: SigningDialogues + state: + args: {} + class_name: SharedState + tendermint_dialogues: + args: {} + class_name: TendermintDialogues +dependencies: + eth_typing: {} + hypothesis: + version: ==6.21.6 + ipfshttpclient: + version: ==0.8.0a2 + open-aea-cli-ipfs: + version: ==1.55.0 + open-aea-test-autonomy: + version: ==0.15.2 + protobuf: + version: <4.25.0,>=4.21.6 + py-ecc: + version: ==6.0.0 + pytest: + version: ==7.2.1 + pytz: + version: ==2022.2.1 + requests: + version: <2.31.2,>=2.28.1 + typing_extensions: + version: '>=3.10.0.2' +is_abstract: true +customs: [] diff --git a/packages/valory/skills/abstract_round_abci/test_tools/__init__.py b/packages/valory/skills/abstract_round_abci/test_tools/__init__.py new file mode 100644 index 0000000..33f9dae --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/test_tools/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Tests package for abstract_round_abci derived skills.""" # pragma: nocover diff --git a/packages/valory/skills/abstract_round_abci/test_tools/abci_app.py b/packages/valory/skills/abstract_round_abci/test_tools/abci_app.py new file mode 100644 index 0000000..10e1260 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/test_tools/abci_app.py @@ -0,0 +1,203 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2022-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""ABCI App test tools.""" + + +from abc import ABC +from enum import Enum +from typing import Dict, Tuple, Type, Union +from unittest.mock import MagicMock + +from packages.valory.skills.abstract_round_abci.base import ( + AbciApp, + AbstractRound, + BaseSynchronizedData, + BaseTxPayload, + DegenerateRound, +) + + +class _ConcreteRound(AbstractRound, ABC): + """ConcreteRound""" + + synchronized_data_class = BaseSynchronizedData + payload_attribute = "" + + def end_block(self) -> Union[None, Tuple[MagicMock, MagicMock]]: + """End block.""" + + def check_payload(self, payload: BaseTxPayload) -> None: + """Check payload.""" + + def process_payload(self, payload: BaseTxPayload) -> None: + """Process payload.""" + + +class ConcreteRoundA(_ConcreteRound): + """Dummy instantiation of the AbstractRound class.""" + + payload_class = BaseTxPayload + + def end_block(self) -> Tuple[MagicMock, MagicMock]: + """End block.""" + return MagicMock(), MagicMock() + + +class ConcreteRoundB(_ConcreteRound): + """Dummy instantiation of the AbstractRound class.""" + + payload_class = BaseTxPayload + + +class ConcreteRoundC(_ConcreteRound): + """Dummy instantiation of the AbstractRound class.""" + + payload_class = BaseTxPayload + + +class ConcreteBackgroundRound(_ConcreteRound): + """Dummy instantiation of the AbstractRound class.""" + + payload_class = BaseTxPayload + + +class ConcreteBackgroundSlashingRound(_ConcreteRound): + """Dummy instantiation of the AbstractRound class.""" + + payload_class = BaseTxPayload + + +class ConcreteTerminationRoundA(_ConcreteRound): + """Dummy instantiation of the AbstractRound class.""" + + payload_class = BaseTxPayload + + +class ConcreteTerminationRoundB(_ConcreteRound): + """Dummy instantiation of the AbstractRound class.""" + + payload_class = BaseTxPayload + + +class ConcreteTerminationRoundC(_ConcreteRound): + """Dummy instantiation of the AbstractRound class.""" + + payload_class = BaseTxPayload + + +class ConcreteSlashingRoundA(_ConcreteRound): + """Dummy instantiation of the AbstractRound class.""" + + payload_class = BaseTxPayload + + +class ConcreteSlashingRoundB(_ConcreteRound): + """Dummy instantiation of the AbstractRound class.""" + + payload_class = BaseTxPayload + + +class ConcreteEvents(Enum): + """Defines dummy events to be used for testing purposes.""" + + TERMINATE = "terminate" + PENDING_OFFENCE = "pending_offence" + SLASH_START = "slash_start" + SLASH_END = "slash_end" + A = "a" + B = "b" + C = "c" + D = "c" + TIMEOUT = "timeout" + + def __str__(self) -> str: + """Get the string representation of the event.""" + return self.value + + +class TerminationAppTest(AbciApp[ConcreteEvents]): + """A dummy Termination abci for testing purposes.""" + + initial_round_cls: Type[AbstractRound] = ConcreteBackgroundRound + transition_function: Dict[ + Type[AbstractRound], Dict[ConcreteEvents, Type[AbstractRound]] + ] = { + ConcreteBackgroundRound: { + ConcreteEvents.TERMINATE: ConcreteTerminationRoundA, + }, + ConcreteTerminationRoundA: { + ConcreteEvents.A: ConcreteTerminationRoundA, + ConcreteEvents.B: ConcreteTerminationRoundB, + ConcreteEvents.C: ConcreteTerminationRoundC, + }, + ConcreteTerminationRoundB: { + ConcreteEvents.B: ConcreteTerminationRoundB, + ConcreteEvents.TIMEOUT: ConcreteTerminationRoundA, + }, + ConcreteTerminationRoundC: { + ConcreteEvents.C: ConcreteTerminationRoundA, + ConcreteEvents.TIMEOUT: ConcreteTerminationRoundC, + }, + } + + +class SlashingAppTest(AbciApp[ConcreteEvents]): + """A dummy Slashing abci for testing purposes.""" + + initial_round_cls: Type[AbstractRound] = ConcreteBackgroundSlashingRound + transition_function: Dict[ + Type[AbstractRound], Dict[ConcreteEvents, Type[AbstractRound]] + ] = { + ConcreteBackgroundSlashingRound: { + ConcreteEvents.SLASH_START: ConcreteSlashingRoundA, + }, + ConcreteSlashingRoundA: {ConcreteEvents.D: ConcreteSlashingRoundB}, + ConcreteSlashingRoundB: { + ConcreteEvents.SLASH_END: DegenerateRound, + }, + } + + +class AbciAppTest(AbciApp[ConcreteEvents]): + """A dummy AbciApp for testing purposes.""" + + TIMEOUT: float = 1.0 + + initial_round_cls: Type[AbstractRound] = ConcreteRoundA + transition_function: Dict[ + Type[AbstractRound], Dict[ConcreteEvents, Type[AbstractRound]] + ] = { + ConcreteRoundA: { + ConcreteEvents.A: ConcreteRoundA, + ConcreteEvents.B: ConcreteRoundB, + ConcreteEvents.C: ConcreteRoundC, + }, + ConcreteRoundB: { + ConcreteEvents.B: ConcreteRoundB, + ConcreteEvents.TIMEOUT: ConcreteRoundA, + }, + ConcreteRoundC: { + ConcreteEvents.C: ConcreteRoundA, + ConcreteEvents.TIMEOUT: ConcreteRoundC, + }, + } + event_to_timeout: Dict[ConcreteEvents, float] = { + ConcreteEvents.TIMEOUT: TIMEOUT, + } diff --git a/packages/valory/skills/abstract_round_abci/test_tools/base.py b/packages/valory/skills/abstract_round_abci/test_tools/base.py new file mode 100644 index 0000000..30640e9 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/test_tools/base.py @@ -0,0 +1,444 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Tests for valory/abstract_round_abci skill's behaviours.""" +import json +from abc import ABC +from copy import copy +from enum import Enum +from pathlib import Path +from tempfile import TemporaryDirectory +from typing import Any, Dict, Type, cast +from unittest import mock +from unittest.mock import MagicMock + +from aea.helpers.transaction.base import SignedMessage +from aea.test_tools.test_skill import BaseSkillTestCase + +from packages.open_aea.protocols.signing import SigningMessage +from packages.valory.connections.http_client.connection import ( + PUBLIC_ID as HTTP_CLIENT_PUBLIC_ID, +) +from packages.valory.connections.ledger.connection import ( + PUBLIC_ID as LEDGER_CONNECTION_PUBLIC_ID, +) +from packages.valory.protocols.contract_api import ContractApiMessage +from packages.valory.protocols.http import HttpMessage +from packages.valory.protocols.ledger_api.message import LedgerApiMessage +from packages.valory.skills.abstract_round_abci.base import ( + AbstractRound, + BaseSynchronizedData, + BaseTxPayload, + OK_CODE, + _MetaPayload, +) +from packages.valory.skills.abstract_round_abci.behaviours import ( + AbstractRoundBehaviour, + BaseBehaviour, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + ContractApiHandler, + HttpHandler, + LedgerApiHandler, + SigningHandler, + TendermintHandler, +) + + +# pylint: disable=protected-access,too-few-public-methods,consider-using-with + + +class FSMBehaviourBaseCase(BaseSkillTestCase, ABC): + """Base case for testing FSMBehaviour classes.""" + + path_to_skill: Path + behaviour: AbstractRoundBehaviour + ledger_handler: LedgerApiHandler + http_handler: HttpHandler + contract_handler: ContractApiHandler + signing_handler: SigningHandler + tendermint_handler: TendermintHandler + old_tx_type_to_payload_cls: Dict[str, Type[BaseTxPayload]] + benchmark_dir: TemporaryDirectory + default_ledger: str = "ethereum" + + @classmethod + def setup_class(cls, **kwargs: Any) -> None: + """Setup the test class.""" + if not hasattr(cls, "path_to_skill"): + raise ValueError(f"No `path_to_skill` set on {cls}") # pragma: nocover + # works once https://github.com/valory-xyz/open-aea/issues/492 is fixed + # we need to store the current value of the meta-class attribute + # _MetaPayload.transaction_type_to_payload_cls, and restore it + # in the teardown function. We do a shallow copy so we avoid + # to modify the old mapping during the execution of the tests. + cls.old_tx_type_to_payload_cls = copy(_MetaPayload.registry) + _MetaPayload.registry = {} + super().setup_class(**kwargs) # pylint: disable=no-value-for-parameter + assert ( + cls._skill.skill_context._agent_context is not None + ), "Agent context not set" # nosec + cls._skill.skill_context._agent_context.identity._default_address_key = ( + cls.default_ledger + ) + cls._skill.skill_context._agent_context._default_ledger_id = cls.default_ledger + behaviour = cls._skill.skill_context.behaviours.main + assert isinstance( + behaviour, AbstractRoundBehaviour + ), f"{behaviour} is not of type {AbstractRoundBehaviour}" + cls.behaviour = behaviour + for attr, handler, handler_type in [ + ("http_handler", cls._skill.skill_context.handlers.http, HttpHandler), + ( + "signing_handler", + cls._skill.skill_context.handlers.signing, + SigningHandler, + ), + ( + "contract_handler", + cls._skill.skill_context.handlers.contract_api, + ContractApiHandler, + ), + ( + "ledger_handler", + cls._skill.skill_context.handlers.ledger_api, + LedgerApiHandler, + ), + ( + "tendermint_handler", + cls._skill.skill_context.handlers.tendermint, + TendermintHandler, + ), + ]: + assert isinstance( + handler, handler_type + ), f"{handler} is not of type {handler_type}" + setattr(cls, attr, handler) + + if kwargs.get("param_overrides") is not None: + for param_name, param_value in kwargs["param_overrides"].items(): + cls.behaviour.context.params.__dict__[param_name] = param_value + + def setup(self, **kwargs: Any) -> None: + """ + Set up the test method. + + Called each time before a test method is called. + + :param kwargs: the keyword arguments passed to _prepare_skill + """ + super().setup(**kwargs) + self.behaviour.setup() + self._skill.skill_context.state.setup() + self._skill.skill_context.state.round_sequence.end_sync() + + self.benchmark_dir = TemporaryDirectory() + self._skill.skill_context.benchmark_tool.__dict__["log_dir"] = Path( + self.benchmark_dir.name + ) + assert ( # nosec + cast(BaseBehaviour, self.behaviour.current_behaviour).behaviour_id + == self.behaviour.initial_behaviour_cls.auto_behaviour_id() + ) + + def fast_forward_to_behaviour( + self, + behaviour: AbstractRoundBehaviour, + behaviour_id: str, + synchronized_data: BaseSynchronizedData, + ) -> None: + """Fast forward the FSM to a behaviour.""" + next_behaviour = {s.auto_behaviour_id(): s for s in behaviour.behaviours}[ + behaviour_id + ] + next_behaviour = cast(Type[BaseBehaviour], next_behaviour) + behaviour.current_behaviour = next_behaviour( + name=next_behaviour.auto_behaviour_id(), skill_context=behaviour.context + ) + self.skill.skill_context.state.round_sequence.abci_app._round_results.append( + synchronized_data + ) + self.skill.skill_context.state.round_sequence.abci_app._extend_previous_rounds_with_current_round() + self.skill.skill_context.behaviours.main._last_round_height = ( + self.skill.skill_context.state.round_sequence.abci_app.current_round_height + ) + self.skill.skill_context.state.round_sequence.abci_app._current_round_cls = ( + next_behaviour.matching_round + ) + # consensus parameters will not be available if the current skill is abstract + consensus_params = getattr( + self.skill.skill_context.params, "consensus_params", None + ) + self.skill.skill_context.state.round_sequence.abci_app._current_round = ( + next_behaviour.matching_round(synchronized_data, consensus_params) + ) + + def mock_ledger_api_request( + self, request_kwargs: Dict, response_kwargs: Dict + ) -> None: + """ + Mock http request. + + :param request_kwargs: keyword arguments for request check. + :param response_kwargs: keyword arguments for mock response. + """ + + self.assert_quantity_in_outbox(1) + actual_ledger_api_message = self.get_message_from_outbox() + assert actual_ledger_api_message is not None, "No message in outbox." # nosec + has_attributes, error_str = self.message_has_attributes( + actual_message=actual_ledger_api_message, + message_type=LedgerApiMessage, + to=str(LEDGER_CONNECTION_PUBLIC_ID), + sender=str(self.skill.skill_context.skill_id), + **request_kwargs, + ) + + assert has_attributes, error_str # nosec + incoming_message = self.build_incoming_message( + message_type=LedgerApiMessage, + dialogue_reference=( + actual_ledger_api_message.dialogue_reference[0], + "stub", + ), + target=actual_ledger_api_message.message_id, + message_id=-1, + to=str(self.skill.skill_context.skill_id), + sender=str(LEDGER_CONNECTION_PUBLIC_ID), + ledger_id=str(LEDGER_CONNECTION_PUBLIC_ID), + **response_kwargs, + ) + self.ledger_handler.handle(incoming_message) + self.behaviour.act_wrapper() + + def mock_contract_api_request( + self, contract_id: str, request_kwargs: Dict, response_kwargs: Dict + ) -> None: + """ + Mock http request. + + :param contract_id: contract id. + :param request_kwargs: keyword arguments for request check. + :param response_kwargs: keyword arguments for mock response. + """ + + self.assert_quantity_in_outbox(1) + actual_contract_ledger_message = self.get_message_from_outbox() + assert ( # nosec + actual_contract_ledger_message is not None + ), "No message in outbox." + has_attributes, error_str = self.message_has_attributes( + actual_message=actual_contract_ledger_message, + message_type=ContractApiMessage, + to=str(LEDGER_CONNECTION_PUBLIC_ID), + sender=str(self.skill.skill_context.skill_id), + ledger_id="ethereum", + contract_id=contract_id, + message_id=1, + **request_kwargs, + ) + assert has_attributes, error_str # nosec + self.behaviour.act_wrapper() + + incoming_message = self.build_incoming_message( + message_type=ContractApiMessage, + dialogue_reference=( + actual_contract_ledger_message.dialogue_reference[0], + "stub", + ), + target=actual_contract_ledger_message.message_id, + message_id=-1, + to=str(self.skill.skill_context.skill_id), + sender=str(LEDGER_CONNECTION_PUBLIC_ID), + ledger_id="ethereum", + contract_id="mock_contract_id", + **response_kwargs, + ) + self.contract_handler.handle(incoming_message) + self.behaviour.act_wrapper() + + def mock_http_request(self, request_kwargs: Dict, response_kwargs: Dict) -> None: + """ + Mock http request. + + :param request_kwargs: keyword arguments for request check. + :param response_kwargs: keyword arguments for mock response. + """ + + self.assert_quantity_in_outbox(1) + actual_http_message = self.get_message_from_outbox() + assert actual_http_message is not None, "No message in outbox." # nosec + has_attributes, error_str = self.message_has_attributes( + actual_message=actual_http_message, + message_type=HttpMessage, + performative=HttpMessage.Performative.REQUEST, + to=str(HTTP_CLIENT_PUBLIC_ID), + sender=str(self.skill.skill_context.skill_id), + **request_kwargs, + ) + assert has_attributes, error_str # nosec + self.behaviour.act_wrapper() + self.assert_quantity_in_outbox(0) + incoming_message = self.build_incoming_message( + message_type=HttpMessage, + dialogue_reference=(actual_http_message.dialogue_reference[0], "stub"), + performative=HttpMessage.Performative.RESPONSE, + target=actual_http_message.message_id, + message_id=-1, + to=str(self.skill.skill_context.skill_id), + sender=str(HTTP_CLIENT_PUBLIC_ID), + **response_kwargs, + ) + self.http_handler.handle(incoming_message) + self.behaviour.act_wrapper() + + def mock_signing_request(self, request_kwargs: Dict, response_kwargs: Dict) -> None: + """Mock signing request.""" + self.assert_quantity_in_decision_making_queue(1) + actual_signing_message = self.get_message_from_decision_maker_inbox() + assert actual_signing_message is not None, "No message in outbox." # nosec + has_attributes, error_str = self.message_has_attributes( + actual_message=actual_signing_message, + message_type=SigningMessage, + to=self.skill.skill_context.decision_maker_address, + sender=str(self.skill.skill_context.skill_id), + **request_kwargs, + ) + assert has_attributes, error_str # nosec + incoming_message = self.build_incoming_message( + message_type=SigningMessage, + dialogue_reference=(actual_signing_message.dialogue_reference[0], "stub"), + target=actual_signing_message.message_id, + message_id=-1, + to=str(self.skill.skill_context.skill_id), + sender=self.skill.skill_context.decision_maker_address, + **response_kwargs, + ) + self.signing_handler.handle(incoming_message) + self.behaviour.act_wrapper() + + def mock_a2a_transaction( + self, + ) -> None: + """Performs mock a2a transaction.""" + + self.mock_signing_request( + request_kwargs=dict( + performative=SigningMessage.Performative.SIGN_MESSAGE, + ), + response_kwargs=dict( + performative=SigningMessage.Performative.SIGNED_MESSAGE, + signed_message=SignedMessage( + ledger_id="ethereum", body="stub_signature" + ), + ), + ) + + self.mock_http_request( + request_kwargs=dict( + method="GET", + headers="", + version="", + body=b"", + ), + response_kwargs=dict( + version="", + status_code=200, + status_text="", + headers="", + body=json.dumps({"result": {"hash": "", "code": OK_CODE}}).encode( + "utf-8" + ), + ), + ) + self.mock_http_request( + request_kwargs=dict( + method="GET", + headers="", + version="", + body=b"", + ), + response_kwargs=dict( + version="", + status_code=200, + status_text="", + headers="", + body=json.dumps({"result": {"tx_result": {"code": OK_CODE}}}).encode( + "utf-8" + ), + ), + ) + + def end_round(self, done_event: Enum) -> None: + """Ends round early to cover `wait_for_end` generator.""" + current_behaviour = cast(BaseBehaviour, self.behaviour.current_behaviour) + if current_behaviour is None: + return + current_behaviour = cast(BaseBehaviour, current_behaviour) + abci_app = current_behaviour.context.state.round_sequence.abci_app + old_round = abci_app._current_round + abci_app._last_round = old_round + abci_app._current_round = abci_app.transition_function[ + current_behaviour.matching_round + ][done_event](abci_app.synchronized_data, context=MagicMock()) + abci_app._previous_rounds.append(old_round) + abci_app._current_round_height += 1 + self.behaviour._process_current_round() + + def _test_done_flag_set(self) -> None: + """Test that, when round ends, the 'done' flag is set.""" + current_behaviour = cast(BaseBehaviour, self.behaviour.current_behaviour) + assert not current_behaviour.is_done() # nosec + with mock.patch.object( + self.behaviour.context.state, "_round_sequence" + ) as mock_round_sequence: + mock_round_sequence.last_round_id = cast( + AbstractRound, current_behaviour.matching_round + ).auto_round_id() + current_behaviour.act_wrapper() + assert current_behaviour.is_done() # nosec + + @classmethod + def teardown_class(cls) -> None: + """Teardown the test class.""" + if getattr(cls, "old_tx_type_to_payload_cls", False): + _MetaPayload.registry = cls.old_tx_type_to_payload_cls + + def teardown(self, **kwargs: Any) -> None: + """Teardown.""" + super().teardown(**kwargs) + self.benchmark_dir.cleanup() + + +class DummyContext: + """Dummy Context class for testing shared state initialization.""" + + class params: + """Dummy param variable.""" + + round_timeout_seconds: float = 1.0 + + _skill: MagicMock = MagicMock() + logger: MagicMock = MagicMock() + skill_id = "dummy_skill_id" + + @property + def is_abstract_component(self) -> bool: + """Mock for is_abstract.""" + return True diff --git a/packages/valory/skills/abstract_round_abci/test_tools/common.py b/packages/valory/skills/abstract_round_abci/test_tools/common.py new file mode 100644 index 0000000..8fdab26 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/test_tools/common.py @@ -0,0 +1,432 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2022-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test common classes.""" +import binascii +import json +import time +from pathlib import Path +from typing import Any, Set, Type, cast +from unittest import mock + +import pytest +from aea.exceptions import AEAActException +from aea.skills.base import SkillContext + +from packages.valory.protocols.contract_api.custom_types import State +from packages.valory.protocols.ledger_api.message import LedgerApiMessage +from packages.valory.skills.abstract_round_abci.base import ( + AbciAppDB, + BaseSynchronizedData, +) +from packages.valory.skills.abstract_round_abci.behaviour_utils import BaseBehaviour +from packages.valory.skills.abstract_round_abci.test_tools.base import ( + FSMBehaviourBaseCase, +) + + +PACKAGE_DIR = Path(__file__).parent.parent +DRAND_VALUE = { + "round": 1416669, + "randomness": "f6be4bf1fa229f22340c1a5b258f809ac4af558200775a67dacb05f0cb258a11", + "signature": ( + "b44d00516f46da3a503f9559a634869b6dc2e5d839e46ec61a090e3032172954929a5" + "d9bd7197d7739fe55db770543c71182562bd0ad20922eb4fe6b8a1062ed21df3b68de" + "44694eb4f20b35262fa9d63aa80ad3f6172dd4d33a663f21179604" + ), + "previous_signature": ( + "903c60a4b937a804001032499a855025573040cb86017c38e2b1c3725286756ce8f33" + "61188789c17336beaf3f9dbf84b0ad3c86add187987a9a0685bc5a303e37b008fba8c" + "44f02a416480dd117a3ff8b8075b1b7362c58af195573623187463" + ), +} + + +class CommonBaseCase(FSMBehaviourBaseCase): + """Base case for testing PriceEstimation FSMBehaviour.""" + + path_to_skill = PACKAGE_DIR = Path(__file__).parent.parent + + +class BaseRandomnessBehaviourTest(CommonBaseCase): + """Test RandomnessBehaviour.""" + + randomness_behaviour_class: Type[BaseBehaviour] + next_behaviour_class: Type[BaseBehaviour] + done_event: Any + + def test_randomness_behaviour( + self, + ) -> None: + """Test RandomnessBehaviour.""" + + self.fast_forward_to_behaviour( + self.behaviour, + self.randomness_behaviour_class.auto_behaviour_id(), + BaseSynchronizedData(AbciAppDB(setup_data={})), + ) + assert ( + cast( + BaseBehaviour, + cast(BaseBehaviour, self.behaviour.current_behaviour), + ).behaviour_id + == self.randomness_behaviour_class.auto_behaviour_id() + ) + self.behaviour.act_wrapper() + self.mock_http_request( + request_kwargs=dict( + method="GET", + headers="", + version="", + body=b"", + url="https://drand.cloudflare.com/public/latest", + ), + response_kwargs=dict( + version="", + status_code=200, + status_text="", + headers="", + body=json.dumps(DRAND_VALUE).encode("utf-8"), + ), + ) + + self.behaviour.act_wrapper() + self.mock_a2a_transaction() + self._test_done_flag_set() + self.end_round(self.done_event) + + behaviour = cast(BaseBehaviour, self.behaviour.current_behaviour) + assert behaviour.behaviour_id == self.next_behaviour_class.auto_behaviour_id() + + def test_invalid_drand_value( + self, + ) -> None: + """Test invalid drand values.""" + self.fast_forward_to_behaviour( + self.behaviour, + self.randomness_behaviour_class.auto_behaviour_id(), + BaseSynchronizedData(AbciAppDB(setup_data={})), + ) + assert ( + cast( + BaseBehaviour, + cast(BaseBehaviour, self.behaviour.current_behaviour), + ).behaviour_id + == self.randomness_behaviour_class.auto_behaviour_id() + ) + self.behaviour.act_wrapper() + + drand_value = DRAND_VALUE.copy() + drand_value["randomness"] = binascii.hexlify(b"randomness_hex").decode() + self.mock_http_request( + request_kwargs=dict( + method="GET", + headers="", + version="", + body=b"", + url="https://drand.cloudflare.com/public/latest", + ), + response_kwargs=dict( + version="", + status_code=200, + status_text="", + headers="", + body=json.dumps(drand_value).encode(), + ), + ) + + def test_invalid_response( + self, + ) -> None: + """Test invalid json response.""" + self.fast_forward_to_behaviour( + self.behaviour, + self.randomness_behaviour_class.auto_behaviour_id(), + BaseSynchronizedData(AbciAppDB(setup_data={})), + ) + assert ( + cast( + BaseBehaviour, + cast(BaseBehaviour, self.behaviour.current_behaviour), + ).behaviour_id + == self.randomness_behaviour_class.auto_behaviour_id() + ) + self.behaviour.act_wrapper() + + self.mock_http_request( + request_kwargs=dict( + method="GET", + headers="", + version="", + body=b"", + url="https://drand.cloudflare.com/public/latest", + ), + response_kwargs=dict( + version="", status_code=200, status_text="", headers="", body=b"" + ), + ) + self.behaviour.act_wrapper() + time.sleep(1) + self.behaviour.act_wrapper() + + def test_max_retries_reached_fallback( + self, + ) -> None: + """Test with max retries reached.""" + self.fast_forward_to_behaviour( + self.behaviour, + self.randomness_behaviour_class.auto_behaviour_id(), + BaseSynchronizedData(AbciAppDB(setup_data={})), + ) + assert ( + cast( + BaseBehaviour, + cast(BaseBehaviour, self.behaviour.current_behaviour), + ).behaviour_id + == self.randomness_behaviour_class.auto_behaviour_id() + ) + self.behaviour.context.randomness_api.__dict__["_frozen"] = False + with mock.patch.object( + self.behaviour.context.randomness_api, + "is_retries_exceeded", + return_value=True, + ): + self.behaviour.act_wrapper() + self.mock_ledger_api_request( + request_kwargs=dict( + performative=LedgerApiMessage.Performative.GET_STATE + ), + response_kwargs=dict( + performative=LedgerApiMessage.Performative.STATE, + state=State(ledger_id="ethereum", body={"hash": "0xa"}), + ), + ) + + self.behaviour.act_wrapper() + self.mock_a2a_transaction() + self._test_done_flag_set() + self.end_round(self.done_event) + + behaviour = cast(BaseBehaviour, self.behaviour.current_behaviour) + assert ( + behaviour.behaviour_id == self.next_behaviour_class.auto_behaviour_id() + ) + self.behaviour.context.randomness_api.__dict__["_frozen"] = True + + def test_max_retries_reached_fallback_fail( + self, + ) -> None: + """Test with max retries reached.""" + self.fast_forward_to_behaviour( + self.behaviour, + self.randomness_behaviour_class.auto_behaviour_id(), + BaseSynchronizedData(AbciAppDB(setup_data={})), + ) + assert ( + cast( + BaseBehaviour, + cast(BaseBehaviour, self.behaviour.current_behaviour), + ).behaviour_id + == self.randomness_behaviour_class.auto_behaviour_id() + ) + self.behaviour.context.randomness_api.__dict__["_frozen"] = False + with mock.patch.object( + self.behaviour.context.randomness_api, + "is_retries_exceeded", + return_value=True, + ): + self.behaviour.act_wrapper() + self.mock_ledger_api_request( + request_kwargs=dict( + performative=LedgerApiMessage.Performative.GET_STATE + ), + response_kwargs=dict( + performative=LedgerApiMessage.Performative.ERROR, + state=State(ledger_id="ethereum", body={}), + ), + ) + + self.behaviour.act_wrapper() + self.behaviour.context.randomness_api.__dict__["_frozen"] = True + + def test_max_retries_reached_fallback_fail_case_2( + self, + ) -> None: + """Test with max retries reached.""" + self.fast_forward_to_behaviour( + self.behaviour, + self.randomness_behaviour_class.auto_behaviour_id(), + BaseSynchronizedData(AbciAppDB(setup_data={})), + ) + assert ( + cast( + BaseBehaviour, + cast(BaseBehaviour, self.behaviour.current_behaviour), + ).behaviour_id + == self.randomness_behaviour_class.auto_behaviour_id() + ) + self.behaviour.context.randomness_api.__dict__["_frozen"] = False + with mock.patch.object( + self.behaviour.context.randomness_api, + "is_retries_exceeded", + return_value=True, + ): + self.behaviour.act_wrapper() + self.mock_ledger_api_request( + request_kwargs=dict( + performative=LedgerApiMessage.Performative.GET_STATE + ), + response_kwargs=dict( + performative=LedgerApiMessage.Performative.STATE, + state=State(ledger_id="ethereum", body={}), + ), + ) + + self.behaviour.act_wrapper() + self.behaviour.context.randomness_api.__dict__["_frozen"] = True + + def test_clean_up( + self, + ) -> None: + """Test when `observed` value is none.""" + self.fast_forward_to_behaviour( + self.behaviour, + self.randomness_behaviour_class.auto_behaviour_id(), + BaseSynchronizedData(AbciAppDB(setup_data={})), + ) + assert ( + cast( + BaseBehaviour, + cast(BaseBehaviour, self.behaviour.current_behaviour), + ).behaviour_id + == self.randomness_behaviour_class.auto_behaviour_id() + ) + self.behaviour.context.randomness_api.retries_info.retries_attempted = ( # pylint: disable=protected-access + 1 + ) + assert self.behaviour.current_behaviour is not None + self.behaviour.current_behaviour.clean_up() + assert ( + self.behaviour.context.randomness_api.retries_info.retries_attempted # pylint: disable=protected-access + == 0 + ) + + +class BaseSelectKeeperBehaviourTest(CommonBaseCase): + """Test SelectKeeperBehaviour.""" + + select_keeper_behaviour_class: Type[BaseBehaviour] + next_behaviour_class: Type[BaseBehaviour] + done_event: Any + _synchronized_data: Type[BaseSynchronizedData] = BaseSynchronizedData + + @mock.patch.object(SkillContext, "agent_address", new_callable=mock.PropertyMock) + @pytest.mark.parametrize( + "blacklisted_keepers", + ( + set(), + {"a_1"}, + {"test_agent_address" + "t" * 24}, + {"a_1" + "t" * 39, "a_2" + "t" * 39, "test_agent_address" + "t" * 24}, + ), + ) + def test_select_keeper( + self, agent_address_mock: mock.Mock, blacklisted_keepers: Set[str] + ) -> None: + """Test select keeper agent.""" + agent_address_mock.return_value = "test_agent_address" + "t" * 24 + participants = ( + self.skill.skill_context.agent_address, + "a_1" + "t" * 39, + "a_2" + "t" * 39, + ) + self.fast_forward_to_behaviour( + behaviour=self.behaviour, + behaviour_id=self.select_keeper_behaviour_class.auto_behaviour_id(), + synchronized_data=self._synchronized_data( + AbciAppDB( + setup_data=AbciAppDB.data_to_lists( + dict( + participants=participants, + most_voted_randomness="56cbde9e9bbcbdcaf92f183c678eaa5288581f06b1c9c7f884ce911776727688", + blacklisted_keepers="".join(blacklisted_keepers), + ) + ), + ) + ), + ) + assert self.behaviour.current_behaviour is not None + assert ( + self.behaviour.current_behaviour.behaviour_id + == self.select_keeper_behaviour_class.auto_behaviour_id() + ) + + if ( + self.behaviour.current_behaviour.synchronized_data.participants + - self.behaviour.current_behaviour.synchronized_data.blacklisted_keepers + ): + self.behaviour.act_wrapper() + self.mock_a2a_transaction() + self._test_done_flag_set() + self.end_round(self.done_event) + assert ( + self.behaviour.current_behaviour.behaviour_id + == self.next_behaviour_class.auto_behaviour_id() + ) + else: + with pytest.raises( + AEAActException, + match="Cannot continue if all the keepers have been blacklisted!", + ): + self.behaviour.act_wrapper() + + def test_select_keeper_preexisting_keeper( + self, + ) -> None: + """Test select keeper agent.""" + participants = (self.skill.skill_context.agent_address, "a_1", "a_2") + preexisting_keeper = next(iter(participants)) + self.fast_forward_to_behaviour( + behaviour=self.behaviour, + behaviour_id=self.select_keeper_behaviour_class.auto_behaviour_id(), + synchronized_data=self._synchronized_data( + AbciAppDB( + setup_data=dict( + participants=[participants], + most_voted_randomness=[ + "56cbde9e9bbcbdcaf92f183c678eaa5288581f06b1c9c7f884ce911776727688" + ], + most_voted_keeper_address=[preexisting_keeper], + ), + ) + ), + ) + assert ( + cast( + BaseBehaviour, + cast(BaseBehaviour, self.behaviour.current_behaviour), + ).behaviour_id + == self.select_keeper_behaviour_class.auto_behaviour_id() + ) + self.behaviour.act_wrapper() + self.mock_a2a_transaction() + self._test_done_flag_set() + self.end_round(self.done_event) + behaviour = cast(BaseBehaviour, self.behaviour.current_behaviour) + assert behaviour.behaviour_id == self.next_behaviour_class.auto_behaviour_id() diff --git a/packages/valory/skills/abstract_round_abci/test_tools/integration.py b/packages/valory/skills/abstract_round_abci/test_tools/integration.py new file mode 100644 index 0000000..9134139 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/test_tools/integration.py @@ -0,0 +1,301 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Integration tests for various transaction settlement skill's failure modes.""" + + +import asyncio +import os +import tempfile +import time +from abc import ABC +from pathlib import Path +from threading import Thread +from typing import Any, Callable, Dict, List, Optional, Tuple, cast + +from aea.crypto.wallet import Wallet +from aea.decision_maker.base import DecisionMaker +from aea.decision_maker.default import DecisionMakerHandler +from aea.identity.base import Identity +from aea.mail.base import Envelope +from aea.multiplexer import Multiplexer +from aea.protocols.base import Address, Message +from aea.skills.base import Handler +from web3 import HTTPProvider, Web3 +from web3.providers import BaseProvider + +from packages.valory.skills.abstract_round_abci.base import BaseSynchronizedData +from packages.valory.skills.abstract_round_abci.behaviour_utils import BaseBehaviour +from packages.valory.skills.abstract_round_abci.handlers import SigningHandler +from packages.valory.skills.abstract_round_abci.test_tools.base import ( + FSMBehaviourBaseCase, +) + + +# pylint: disable=protected-access,too-many-ancestors,unbalanced-tuple-unpacking,too-many-locals,consider-using-with,unspecified-encoding,too-many-arguments,unidiomatic-typecheck + +HandlersType = List[Optional[Handler]] +ExpectedContentType = List[ + Optional[ + Dict[ + str, + Any, + ] + ] +] +ExpectedTypesType = List[ + Optional[ + Dict[ + str, + Any, + ] + ] +] + + +class IntegrationBaseCase(FSMBehaviourBaseCase, ABC): + """Base test class for integration tests.""" + + running_loop: asyncio.AbstractEventLoop + thread_loop: Thread + multiplexer: Multiplexer + decision_maker: DecisionMaker + agents: Dict[str, Address] = { + "0xBcd4042DE499D14e55001CcbB24a551F3b954096": "0xf214f2b2cd398c806f84e317254e0f0b801d0643303237d97a22a48e01628897", + "0x71bE63f3384f5fb98995898A86B02Fb2426c5788": "0x701b615bbdfb9de65240bc28bd21bbc0d996645a3dd57e7b12bc2bdf6f192c82", + "0xFABB0ac9d68B0B445fB7357272Ff202C5651694a": "0xa267530f49f8280200edf313ee7af6b827f2a8bce2897751d06a843f644967b1", + "0x1CBd3b2770909D4e10f157cABC84C7264073C9Ec": "0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd", + } + current_agent: Address + ROOT_DIR: Path + make_ledger_api_connection_callable: Callable + + @classmethod + def _setup_class(cls, **kwargs: Any) -> None: + """Setup class.""" + + @classmethod + def setup_class(cls, **kwargs: Any) -> None: + """Setup.""" + super().setup_class() + + # set up a multiplexer with the required connections + cls.running_loop = asyncio.new_event_loop() + cls.thread_loop = Thread(target=cls.running_loop.run_forever) + cls.thread_loop.start() + cls.multiplexer = Multiplexer( + [cls.make_ledger_api_connection_callable()], loop=cls.running_loop + ) + cls.multiplexer.connect() + + # hardhat configuration + # setup decision maker + with tempfile.TemporaryDirectory() as temp_dir: + fp = os.path.join(temp_dir, "key.txt") + f = open(fp, "w") + f.write(cls.agents[next(iter(cls.agents))]) + f.close() + wallet = Wallet(private_key_paths={"ethereum": str(fp)}) + identity = Identity( + "test_agent_name", + addresses=wallet.addresses, + public_keys=wallet.public_keys, + default_address_key="ethereum", + ) + cls._skill._skill_context._agent_context._identity = identity + cls.current_agent = identity.address + + cls.decision_maker = DecisionMaker( + decision_maker_handler=DecisionMakerHandler(identity, wallet, {}) + ) + cls._skill._skill_context._agent_context._decision_maker_message_queue = ( + cls.decision_maker.message_in_queue + ) + cls._skill.skill_context._agent_context._decision_maker_address = ( + "decision_maker" + ) + + @classmethod + def teardown_class(cls) -> None: + """Tear down the multiplexer.""" + cls.multiplexer.disconnect() + cls.running_loop.call_soon_threadsafe(cls.running_loop.stop) + cls.thread_loop.join() + super().teardown_class() + + def get_message_from_decision_maker_inbox(self) -> Optional[Message]: + """Get message from decision maker inbox.""" + if self._skill.skill_context.decision_maker_message_queue.empty(): + return None + return self._skill.skill_context.decision_maker_message_queue.protected_get( + self.decision_maker._queue_access_code, block=True + ) + + def process_message_cycle( + self, + handler: Optional[Handler] = None, + expected_content: Optional[Dict] = None, + expected_types: Optional[Dict] = None, + mining_interval_secs: float = 0, + ) -> Optional[Message]: + """ + Processes one request-response type message cycle. + + Steps: + 1. Calls act on behaviour to generate outgoing message + 2. Checks for message in outbox + 3. Sends message to multiplexer and waits for response. + 4. Passes message to handler + 5. Calls act on behaviour to process incoming message + + :param handler: the handler to handle a potential incoming message + :param expected_content: the content to be expected + :param expected_types: the types to be expected + :param mining_interval_secs: the mining interval used in the tests + :return: the incoming message + """ + if expected_types and tuple(expected_types)[0] == "transaction_receipt": + time.sleep(mining_interval_secs) # pragma: no cover + self.behaviour.act_wrapper() + incoming_message = None + + if type(handler) == SigningHandler: + self.assert_quantity_in_decision_making_queue(1) + message = self.get_message_from_decision_maker_inbox() + assert message is not None, "No message in outbox." # nosec + self.decision_maker.handle(message) + if handler is not None: + incoming_message = self.decision_maker.message_out_queue.get(block=True) + assert isinstance(incoming_message, Message) # nosec + else: + self.assert_quantity_in_outbox(1) + message = self.get_message_from_outbox() + assert message is not None, "No message in outbox." # nosec + self.multiplexer.put( + Envelope( + to=message.to, + sender=message.sender, + message=message, + context=None, + ) + ) + if handler is not None: + envelope = self.multiplexer.get(block=True) + assert envelope is not None, "No envelope" # nosec + incoming_message = envelope.message + assert isinstance(incoming_message, Message) # nosec + + if handler is not None: + assert incoming_message is not None # nosec + if expected_content is not None: + assert all( # nosec + [ + incoming_message._body.get(key, None) == value + for key, value in expected_content.items() + ] + ), f"Actual content: {incoming_message._body}, expected: {expected_content}" + + if expected_types is not None: + assert all( # nosec + [ + type(incoming_message._body.get(key, None)) == value_type + for key, value_type in expected_types.items() + ] + ), "Content type mismatch" + handler.handle(incoming_message) + return incoming_message + return None + + def process_n_messages( + self, + ncycles: int, + synchronized_data: Optional[BaseSynchronizedData] = None, + behaviour_id: Optional[str] = None, + handlers: Optional[HandlersType] = None, + expected_content: Optional[ExpectedContentType] = None, + expected_types: Optional[ExpectedTypesType] = None, + fail_send_a2a: bool = False, + mining_interval_secs: float = 0, + ) -> Tuple[Optional[Message], ...]: + """ + Process n message cycles. + + :param behaviour_id: the behaviour to fast forward to + :param ncycles: the number of message cycles to process + :param synchronized_data: a synchronized_data + :param handlers: a list of handlers + :param expected_content: the expected_content + :param expected_types: the expected type + :param fail_send_a2a: flag that indicates whether we want to simulate a failure in the `send_a2a_transaction` + :param mining_interval_secs: the mining interval used in the tests. + + :return: tuple of incoming messages + """ + handlers = [None] * ncycles if handlers is None else handlers + expected_content = ( + [None] * ncycles if expected_content is None else expected_content + ) + expected_types = [None] * ncycles if expected_types is None else expected_types + assert ( # nosec + len(expected_content) == len(expected_types) + and len(expected_content) == len(handlers) + and len(expected_content) == ncycles + ), "Number of cycles, handlers, contents and types does not match" + + if behaviour_id is not None and synchronized_data is not None: + self.fast_forward_to_behaviour( + behaviour=self.behaviour, + behaviour_id=behaviour_id, + synchronized_data=synchronized_data, + ) + assert ( # nosec + cast(BaseBehaviour, self.behaviour.current_behaviour).behaviour_id + == behaviour_id + ) + + incoming_messages = [] + for i in range(ncycles): + incoming_message = self.process_message_cycle( + handlers[i], + expected_content[i], + expected_types[i], + mining_interval_secs, + ) + incoming_messages.append(incoming_message) + + self.behaviour.act_wrapper() + if not fail_send_a2a: + self.mock_a2a_transaction() + return tuple(incoming_messages) + + +class HardHatHelperIntegration(IntegrationBaseCase, ABC): # pragma: no cover + """Base test class for integration tests with HardHat provider.""" + + hardhat_provider: BaseProvider + + @classmethod + def setup_class(cls, **kwargs: Any) -> None: + """Setup.""" + super().setup_class() + + # create an API for HardHat + cls.hardhat_provider = Web3( + provider=HTTPProvider("http://localhost:8545") + ).provider diff --git a/packages/valory/skills/abstract_round_abci/test_tools/rounds.py b/packages/valory/skills/abstract_round_abci/test_tools/rounds.py new file mode 100644 index 0000000..77f50c8 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/test_tools/rounds.py @@ -0,0 +1,599 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2022-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test tools for testing rounds.""" + +import re +from copy import deepcopy +from dataclasses import dataclass +from enum import Enum +from typing import ( + Any, + Callable, + FrozenSet, + Generator, + List, + Mapping, + Optional, + Tuple, + Type, +) +from unittest import mock + +import pytest + +from packages.valory.skills.abstract_round_abci.base import ( + ABCIAppInternalError, + AbciAppDB, + AbstractRound, + BaseSynchronizedData, + BaseTxPayload, + CollectDifferentUntilAllRound, + CollectDifferentUntilThresholdRound, + CollectNonEmptyUntilThresholdRound, + CollectSameUntilAllRound, + CollectSameUntilThresholdRound, + CollectionRound, + OnlyKeeperSendsRound, + TransactionNotValidError, + VotingRound, +) + + +MAX_PARTICIPANTS: int = 4 + + +def get_participants() -> FrozenSet[str]: + """Participants""" + return frozenset([f"agent_{i}" for i in range(MAX_PARTICIPANTS)]) + + +class DummyEvent(Enum): + """Dummy Event""" + + DONE = "done" + ROUND_TIMEOUT = "round_timeout" + NO_MAJORITY = "no_majority" + RESET_TIMEOUT = "reset_timeout" + NEGATIVE = "negative" + NONE = "none" + FAIL = "fail" + + +@dataclass(frozen=True) +class DummyTxPayload(BaseTxPayload): + """Dummy Transaction Payload.""" + + value: Optional[str] = None + vote: Optional[bool] = None + + +class DummySynchronizedData(BaseSynchronizedData): + """Dummy synchronized data for tests.""" + + +def get_dummy_tx_payloads( + participants: FrozenSet[str], + value: Any = None, + vote: Optional[bool] = False, + is_value_none: bool = False, + is_vote_none: bool = False, +) -> List[DummyTxPayload]: + """Returns a list of DummyTxPayload objects.""" + return [ + DummyTxPayload( + sender=agent, + value=(value or agent) if not is_value_none else value, + vote=vote if not is_vote_none else None, + ) + for agent in sorted(participants) + ] + + +class DummyRound(AbstractRound): + """Dummy round.""" + + payload_class = DummyTxPayload + payload_attribute = "value" + synchronized_data_class = BaseSynchronizedData + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: + """end_block method.""" + + +class DummyCollectionRound(CollectionRound, DummyRound): + """Dummy Class for CollectionRound""" + + +class DummyCollectDifferentUntilAllRound(CollectDifferentUntilAllRound, DummyRound): + """Dummy Class for CollectDifferentUntilAllRound""" + + +class DummyCollectSameUntilAllRound(CollectSameUntilAllRound, DummyRound): + """Dummy Class for CollectSameUntilThresholdRound""" + + +class DummyCollectDifferentUntilThresholdRound( + CollectDifferentUntilThresholdRound, DummyRound +): + """Dummy Class for CollectDifferentUntilThresholdRound""" + + +class DummyCollectSameUntilThresholdRound(CollectSameUntilThresholdRound, DummyRound): + """Dummy Class for CollectSameUntilThresholdRound""" + + +class DummyOnlyKeeperSendsRound(OnlyKeeperSendsRound, DummyRound): + """Dummy Class for OnlyKeeperSendsRound""" + + fail_event = "FAIL_EVENT" + + +class DummyVotingRound(VotingRound, DummyRound): + """Dummy Class for VotingRound""" + + +class DummyCollectNonEmptyUntilThresholdRound( + CollectNonEmptyUntilThresholdRound, DummyRound +): + """Dummy Class for `CollectNonEmptyUntilThresholdRound`""" + + +class BaseRoundTestClass: # pylint: disable=too-few-public-methods + """Base test class.""" + + synchronized_data: BaseSynchronizedData + participants: FrozenSet[str] + + _synchronized_data_class: Type[BaseSynchronizedData] + _event_class: Any + + def setup( + self, + ) -> None: + """Setup test class.""" + + self.participants = get_participants() + self.synchronized_data = self._synchronized_data_class( + db=AbciAppDB( + setup_data=dict( + participants=[tuple(self.participants)], + all_participants=[tuple(self.participants)], + consensus_threshold=[3], + safe_contract_address=["test_address"], + ), + ) + ) + + def _test_no_majority_event(self, round_obj: AbstractRound) -> None: + """Test the NO_MAJORITY event.""" + with mock.patch.object(round_obj, "is_majority_possible", return_value=False): + result = round_obj.end_block() + assert result is not None + _, event = result + assert event == self._event_class.NO_MAJORITY + + @staticmethod + def _complete_run( + test_runner: Generator, iter_count: int = MAX_PARTICIPANTS + ) -> None: + """ + This method represents logic to execute test logic defined in _test_round method. + + _test_round should follow these steps + + 1. process first payload + 2. yield test_round + 3. test collection, end_block and thresholds + 4. process rest of the payloads + 5. yield test_round + 6. yield synchronized_data, event ( returned from end_block ) + 7. test synchronized_data and event + + :param test_runner: test runner + :param iter_count: iter_count + """ + + for _ in range(iter_count): + next(test_runner) + + +class BaseCollectDifferentUntilAllRoundTest( # pylint: disable=too-few-public-methods + BaseRoundTestClass +): + """Tests for rounds derived from CollectDifferentUntilAllRound.""" + + def _test_round( + self, + test_round: CollectDifferentUntilAllRound, + round_payloads: List[BaseTxPayload], + synchronized_data_update_fn: Callable, + synchronized_data_attr_checks: List[Callable], + exit_event: Any, + ) -> Generator: + """Test round.""" + + first_payload = round_payloads.pop(0) + test_round.process_payload(first_payload) + + yield test_round + assert test_round.collection[first_payload.sender] == first_payload + assert not test_round.collection_threshold_reached + assert test_round.end_block() is None + + for payload in round_payloads: + test_round.process_payload(payload) + yield test_round + assert test_round.collection_threshold_reached + + actual_next_synchronized_data = synchronized_data_update_fn( + deepcopy(self.synchronized_data), test_round + ) + + res = test_round.end_block() + yield res + if exit_event is None: + assert res is exit_event + else: + assert res is not None + synchronized_data, event = res + for behaviour_attr_getter in synchronized_data_attr_checks: + assert behaviour_attr_getter( + synchronized_data + ) == behaviour_attr_getter(actual_next_synchronized_data) + assert event == exit_event + yield + + +class BaseCollectSameUntilAllRoundTest( + BaseRoundTestClass +): # pylint: disable=too-few-public-methods + """Tests for rounds derived from CollectSameUntilAllRound.""" + + def _test_round( # pylint: disable=too-many-arguments,too-many-locals + self, + test_round: CollectSameUntilAllRound, + round_payloads: Mapping[str, BaseTxPayload], + synchronized_data_update_fn: Callable, + synchronized_data_attr_checks: List[Callable], + most_voted_payload: Any, + exit_event: Any, + finished: bool, + ) -> Generator: + """Test rounds derived from CollectionRound.""" + + (_, first_payload), *payloads = round_payloads.items() + + test_round.process_payload(first_payload) + yield test_round + assert test_round.collection[first_payload.sender] == first_payload + assert not test_round.collection_threshold_reached + assert test_round.end_block() is None + + with pytest.raises( + ABCIAppInternalError, + match="internal error: 1 votes are not enough for `CollectSameUntilAllRound`. " + f"Expected: `n_votes = max_participants = {MAX_PARTICIPANTS}`", + ): + _ = test_round.common_payload + + for _, payload in payloads: + test_round.process_payload(payload) + yield test_round + if finished: + assert test_round.collection_threshold_reached + assert test_round.common_payload == most_voted_payload + + actual_next_synchronized_data = synchronized_data_update_fn( + deepcopy(self.synchronized_data), test_round + ) + res = test_round.end_block() + yield res + assert res is not None + + synchronized_data, event = res + + for behaviour_attr_getter in synchronized_data_attr_checks: + assert behaviour_attr_getter(synchronized_data) == behaviour_attr_getter( + actual_next_synchronized_data + ), f"Mismatch in synchronized_data. Actual:\n{behaviour_attr_getter(synchronized_data)}\nExpected:\n{behaviour_attr_getter(actual_next_synchronized_data)}" + assert event == exit_event + yield + + +class BaseCollectSameUntilThresholdRoundTest( # pylint: disable=too-few-public-methods + BaseRoundTestClass +): + """Tests for rounds derived from CollectSameUntilThresholdRound.""" + + def _test_round( # pylint: disable=too-many-arguments,too-many-locals + self, + test_round: CollectSameUntilThresholdRound, + round_payloads: Mapping[str, BaseTxPayload], + synchronized_data_update_fn: Callable, + synchronized_data_attr_checks: List[Callable], + most_voted_payload: Any, + exit_event: Any, + ) -> Generator: + """Test rounds derived from CollectionRound.""" + + (_, first_payload), *payloads = round_payloads.items() + + test_round.process_payload(first_payload) + yield test_round + assert test_round.collection[first_payload.sender] == first_payload + assert not test_round.threshold_reached + assert test_round.end_block() is None + + self._test_no_majority_event(test_round) + with pytest.raises(ABCIAppInternalError, match="not enough votes"): + _ = test_round.most_voted_payload + + for _, payload in payloads: + test_round.process_payload(payload) + yield test_round + assert test_round.threshold_reached + assert test_round.most_voted_payload == most_voted_payload + + actual_next_synchronized_data = synchronized_data_update_fn( + deepcopy(self.synchronized_data), test_round + ) + res = test_round.end_block() + yield res + assert res is not None + + synchronized_data, event = res + + for behaviour_attr_getter in synchronized_data_attr_checks: + assert behaviour_attr_getter(synchronized_data) == behaviour_attr_getter( + actual_next_synchronized_data + ), f"Mismatch in synchronized_data. Actual:\n{behaviour_attr_getter(synchronized_data)}\nExpected:\n{behaviour_attr_getter(actual_next_synchronized_data)}" + assert event == exit_event + yield + + +class BaseOnlyKeeperSendsRoundTest( # pylint: disable=too-few-public-methods + BaseRoundTestClass +): + """Tests for rounds derived from OnlyKeeperSendsRound.""" + + def _test_round( + self, + test_round: OnlyKeeperSendsRound, + keeper_payloads: BaseTxPayload, + synchronized_data_update_fn: Callable, + synchronized_data_attr_checks: List[Callable], + exit_event: Any, + ) -> Generator: + """Test for rounds derived from OnlyKeeperSendsRound.""" + + assert test_round.end_block() is None + assert test_round.keeper_payload is None + + test_round.process_payload(keeper_payloads) + yield test_round + assert test_round.keeper_payload is not None + + yield test_round + actual_next_synchronized_data = synchronized_data_update_fn( + deepcopy(self.synchronized_data), test_round + ) + res = test_round.end_block() + yield res + assert res is not None + + synchronized_data, event = res + + for behaviour_attr_getter in synchronized_data_attr_checks: + assert behaviour_attr_getter(synchronized_data) == behaviour_attr_getter( + actual_next_synchronized_data + ), f"Mismatch in synchronized_data. Actual:\n{behaviour_attr_getter(synchronized_data)}\nExpected:\n{behaviour_attr_getter(actual_next_synchronized_data)}" + assert event == exit_event + yield + + +class BaseVotingRoundTest(BaseRoundTestClass): # pylint: disable=too-few-public-methods + """Tests for rounds derived from VotingRound.""" + + def _test_round( # pylint: disable=too-many-arguments,too-many-locals + self, + test_round: VotingRound, + round_payloads: Mapping[str, BaseTxPayload], + synchronized_data_update_fn: Callable, + synchronized_data_attr_checks: List[Callable], + exit_event: Any, + threshold_check: Callable, + ) -> Generator: + """Test for rounds derived from VotingRound.""" + + (_, first_payload), *payloads = round_payloads.items() + + test_round.process_payload(first_payload) + yield test_round + assert not threshold_check(test_round) # negative_vote_threshold_reached + assert test_round.end_block() is None + self._test_no_majority_event(test_round) + + for _, payload in payloads: + test_round.process_payload(payload) + yield test_round + assert threshold_check(test_round) + + actual_next_synchronized_data = synchronized_data_update_fn( + deepcopy(self.synchronized_data), test_round + ) + res = test_round.end_block() + yield res + assert res is not None + + synchronized_data, event = res + + for behaviour_attr_getter in synchronized_data_attr_checks: + assert behaviour_attr_getter(synchronized_data) == behaviour_attr_getter( + actual_next_synchronized_data + ), f"Mismatch in synchronized_data. Actual:\n{behaviour_attr_getter(synchronized_data)}\nExpected:\n{behaviour_attr_getter(actual_next_synchronized_data)}" + assert event == exit_event + yield + + def _test_voting_round_positive( + self, + test_round: VotingRound, + round_payloads: Mapping[str, BaseTxPayload], + synchronized_data_update_fn: Callable, + synchronized_data_attr_checks: List[Callable], + exit_event: Any, + ) -> Generator: + """Test for rounds derived from VotingRound.""" + + return self._test_round( + test_round, + round_payloads, + synchronized_data_update_fn, + synchronized_data_attr_checks, + exit_event, + threshold_check=lambda x: x.positive_vote_threshold_reached, + ) + + def _test_voting_round_negative( + self, + test_round: VotingRound, + round_payloads: Mapping[str, BaseTxPayload], + synchronized_data_update_fn: Callable, + synchronized_data_attr_checks: List[Callable], + exit_event: Any, + ) -> Generator: + """Test for rounds derived from VotingRound.""" + + return self._test_round( + test_round, + round_payloads, + synchronized_data_update_fn, + synchronized_data_attr_checks, + exit_event, + threshold_check=lambda x: x.negative_vote_threshold_reached, + ) + + def _test_voting_round_none( + self, + test_round: VotingRound, + round_payloads: Mapping[str, BaseTxPayload], + synchronized_data_update_fn: Callable, + synchronized_data_attr_checks: List[Callable], + exit_event: Any, + ) -> Generator: + """Test for rounds derived from VotingRound.""" + + return self._test_round( + test_round, + round_payloads, + synchronized_data_update_fn, + synchronized_data_attr_checks, + exit_event, + threshold_check=lambda x: x.none_vote_threshold_reached, + ) + + +class BaseCollectDifferentUntilThresholdRoundTest( # pylint: disable=too-few-public-methods + BaseRoundTestClass +): + """Tests for rounds derived from CollectDifferentUntilThresholdRound.""" + + def _test_round( + self, + test_round: CollectDifferentUntilThresholdRound, + round_payloads: Mapping[str, BaseTxPayload], + synchronized_data_update_fn: Callable, + synchronized_data_attr_checks: List[Callable], + exit_event: Any, + ) -> Generator: + """Test for rounds derived from CollectDifferentUntilThresholdRound.""" + + (_, first_payload), *payloads = round_payloads.items() + + test_round.process_payload(first_payload) + yield test_round + assert not test_round.collection_threshold_reached + assert test_round.end_block() is None + + for _, payload in payloads: + test_round.process_payload(payload) + yield test_round + assert test_round.collection_threshold_reached + + actual_next_synchronized_data = synchronized_data_update_fn( + deepcopy(self.synchronized_data), test_round + ) + + res = test_round.end_block() + yield res + assert res is not None + + synchronized_data, event = res + + for behaviour_attr_getter in synchronized_data_attr_checks: + assert behaviour_attr_getter(synchronized_data) == behaviour_attr_getter( + actual_next_synchronized_data + ), f"Mismatch in synchronized_data. Actual:\n{behaviour_attr_getter(synchronized_data)}\nExpected:\n{behaviour_attr_getter(actual_next_synchronized_data)}" + assert event == exit_event + yield + + +class BaseCollectNonEmptyUntilThresholdRound( # pylint: disable=too-few-public-methods + BaseCollectDifferentUntilThresholdRoundTest +): + """Tests for rounds derived from `CollectNonEmptyUntilThresholdRound`.""" + + +class _BaseRoundTestClass(BaseRoundTestClass): # pylint: disable=too-few-public-methods + """Base test class.""" + + synchronized_data: BaseSynchronizedData + participants: FrozenSet[str] + tx_payloads: List[DummyTxPayload] + + _synchronized_data_class = DummySynchronizedData + + def setup( + self, + ) -> None: + """Setup test class.""" + + super().setup() + self.tx_payloads = get_dummy_tx_payloads(self.participants) + + @staticmethod + def _test_payload_with_wrong_round_count( + test_round: AbstractRound, + value: Optional[str] = None, + vote: Optional[bool] = None, + ) -> None: + """Test errors raised by payloads with wrong round count.""" + payload_with_wrong_round_count = DummyTxPayload("sender", value, vote) + object.__setattr__(payload_with_wrong_round_count, "round_count", 0) + with pytest.raises( + TransactionNotValidError, + match=re.escape("Expected round count -1 and got 0."), + ): + test_round.check_payload(payload=payload_with_wrong_round_count) + + with pytest.raises( + ABCIAppInternalError, + match=re.escape("Expected round count -1 and got 0."), + ): + test_round.process_payload(payload=payload_with_wrong_round_count) diff --git a/packages/valory/skills/abstract_round_abci/tests/__init__.py b/packages/valory/skills/abstract_round_abci/tests/__init__.py new file mode 100644 index 0000000..7561108 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Tests for valory/abstract_round_abci skill.""" + +from hypothesis import settings # pragma: nocover + + +CI = "CI" # pragma: nocover + +settings.register_profile(CI, deadline=5000) # pragma: nocover diff --git a/packages/valory/skills/abstract_round_abci/tests/conftest.py b/packages/valory/skills/abstract_round_abci/tests/conftest.py new file mode 100644 index 0000000..5dde4f1 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/conftest.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Conftest module for io tests.""" + +import os +import shutil +from contextlib import suppress +from pathlib import Path +from typing import Dict, Generator + +import pytest +from hypothesis import settings + +from packages.valory.skills.abstract_round_abci.io_.store import StoredJSONType +from packages.valory.skills.abstract_round_abci.models import MIN_RESET_PAUSE_DURATION + + +# pylint: skip-file + + +CI = "CI" +PACKAGE_DIR = Path(__file__).parent.parent +settings.register_profile(CI, deadline=5000) +profile_name = ("default", "CI")[bool(os.getenv("CI"))] + + +@pytest.fixture +def dummy_obj() -> StoredJSONType: + """A dummy custom object to test the storing with.""" + return {"test_col": ["test_val_1", "test_val_2"]} + + +@pytest.fixture +def dummy_multiple_obj(dummy_obj: StoredJSONType) -> Dict[str, StoredJSONType]: + """Many dummy custom objects to test the storing with.""" + return {f"test_obj_{i}": dummy_obj for i in range(10)} + + +@pytest.fixture(scope="session", autouse=True) +def hypothesis_cleanup() -> Generator: + """Fixture to remove hypothesis directory after tests.""" + yield + hypothesis_dir = PACKAGE_DIR / ".hypothesis" + if hypothesis_dir.exists(): + with suppress(OSError, PermissionError): # pragma: nocover + shutil.rmtree(hypothesis_dir) + + +# We do not care about these keys but need to set them in the behaviours' tests, +# because `packages.valory.skills.abstract_round_abci.models._ensure` is used. +irrelevant_genesis_config = { + "consensus_params": { + "block": {"max_bytes": "str", "max_gas": "str", "time_iota_ms": "str"}, + "evidence": { + "max_age_num_blocks": "str", + "max_age_duration": "str", + "max_bytes": "str", + }, + "validator": {"pub_key_types": ["str"]}, + "version": {}, + }, + "genesis_time": "str", + "chain_id": "str", + "voting_power": "str", +} +irrelevant_config = { + "tendermint_url": "str", + "max_healthcheck": 0, + "round_timeout_seconds": 0.0, + "sleep_time": 0, + "retry_timeout": 0, + "retry_attempts": 0, + "keeper_timeout": 0.0, + "reset_pause_duration": MIN_RESET_PAUSE_DURATION, + "drand_public_key": "str", + "tendermint_com_url": "str", + "tendermint_max_retries": 0, + "reset_tendermint_after": 0, + "cleanup_history_depth": 0, + "voting_power": 0, + "tendermint_check_sleep_delay": 0, + "cleanup_history_depth_current": None, + "request_timeout": 0.0, + "request_retry_delay": 0.0, + "tx_timeout": 0.0, + "max_attempts": 0, + "service_registry_address": None, + "on_chain_service_id": None, + "share_tm_config_on_startup": False, + "tendermint_p2p_url": "str", + "setup": {}, + "genesis_config": irrelevant_genesis_config, + "use_termination": False, + "use_slashing": False, + "slash_cooldown_hours": 3, + "slash_threshold_amount": 10_000_000_000_000_000, + "light_slash_unit_amount": 5_000_000_000_000_000, + "serious_slash_unit_amount": 8_000_000_000_000_000, +} diff --git a/packages/valory/skills/abstract_round_abci/tests/data/__init__.py b/packages/valory/skills/abstract_round_abci/tests/data/__init__.py new file mode 100644 index 0000000..7266f78 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/data/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Tests for abstract_round_abci/test_tools""" diff --git a/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/__init__.py b/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/__init__.py new file mode 100644 index 0000000..c378e8c --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the implementation of the default skill.""" + +from pathlib import Path + +from aea.configurations.base import PublicId + + +PUBLIC_ID = PublicId.from_str("dummy/dummy_abci:0.1.0") +PATH_TO_SKILL = Path(__file__).parent diff --git a/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/behaviours.py b/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/behaviours.py new file mode 100644 index 0000000..ee99805 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/behaviours.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2022-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains round behaviours of DummyAbciApp.""" + +from abc import ABC +from collections import deque +from typing import Deque, Generator, Set, Type, cast + +from packages.valory.skills.abstract_round_abci.base import AbstractRound +from packages.valory.skills.abstract_round_abci.behaviours import ( + AbstractRoundBehaviour, + BaseBehaviour, +) +from packages.valory.skills.abstract_round_abci.common import ( + RandomnessBehaviour, + SelectKeeperBehaviour, +) +from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci.models import ( + Params, +) +from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci.payloads import ( + DummyFinalPayload, + DummyKeeperSelectionPayload, + DummyRandomnessPayload, + DummyStartingPayload, +) +from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci.rounds import ( + DummyAbciApp, + DummyFinalRound, + DummyKeeperSelectionRound, + DummyRandomnessRound, + DummyStartingRound, + SynchronizedData, +) + + +class DummyBaseBehaviour(BaseBehaviour, ABC): + """Base behaviour for the common apps' skill.""" + + @property + def synchronized_data(self) -> SynchronizedData: + """Return the synchronized data.""" + return cast(SynchronizedData, super().synchronized_data) + + @property + def params(self) -> Params: + """Return the params.""" + return cast(Params, super().params) + + +class DummyStartingBehaviour(DummyBaseBehaviour): + """DummyStartingBehaviour""" + + behaviour_id: str = "dummy_starting" + matching_round: Type[AbstractRound] = DummyStartingRound + + def async_act(self) -> Generator: + """Do the act, supporting asynchronous execution.""" + + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + content = "dummy" + sender = self.context.agent_address + payload = DummyStartingPayload(sender=sender, content=content) + + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + yield from self.send_a2a_transaction(payload) + yield from self.wait_until_round_end() + + self.set_done() + + +class DummyRandomnessBehaviour(RandomnessBehaviour): + """DummyRandomnessBehaviour""" + + behaviour_id: str = "dummy_randomness" + matching_round: Type[AbstractRound] = DummyRandomnessRound + payload_class = DummyRandomnessPayload + + +class DummyKeeperSelectionBehaviour(SelectKeeperBehaviour): + """DummyKeeperSelectionBehaviour""" + + behaviour_id: str = "dummy_keeper_selection" + matching_round: Type[AbstractRound] = DummyKeeperSelectionRound + payload_class = DummyKeeperSelectionPayload + + @staticmethod + def serialized_keepers(keepers: Deque[str], keeper_retries: int = 1) -> str: + """Get the keepers serialized.""" + if not keepers: + return "" + return keeper_retries.to_bytes(32, "big").hex() + "".join(keepers) + + def async_act(self) -> Generator: + """Do the act, supporting asynchronous execution.""" + + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + keepers = deque((self._select_keeper(),)) + payload = self.payload_class( + self.context.agent_address, self.serialized_keepers(keepers) + ) + + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + yield from self.send_a2a_transaction(payload) + yield from self.wait_until_round_end() + + self.set_done() + + +class DummyFinalBehaviour(DummyBaseBehaviour): + """DummyFinalBehaviour""" + + behaviour_id: str = "dummy_final" + matching_round: Type[AbstractRound] = DummyFinalRound + + def async_act(self) -> Generator: + """Do the act, supporting asynchronous execution.""" + + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + content = True + sender = self.context.agent_address + payload = DummyFinalPayload(sender=sender, content=content) + + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + yield from self.send_a2a_transaction(payload) + yield from self.wait_until_round_end() + + self.set_done() + + +class DummyRoundBehaviour(AbstractRoundBehaviour): + """DummyRoundBehaviour""" + + initial_behaviour_cls = DummyStartingBehaviour + abci_app_cls = DummyAbciApp + behaviours: Set[Type[BaseBehaviour]] = { + DummyFinalBehaviour, # type: ignore + DummyKeeperSelectionBehaviour, # type: ignore + DummyRandomnessBehaviour, # type: ignore + DummyStartingBehaviour, # type: ignore + } diff --git a/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/dialogues.py b/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/dialogues.py new file mode 100644 index 0000000..24aa94d --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/dialogues.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the dialogues of the DummyAbciApp.""" + +from packages.valory.skills.abstract_round_abci.dialogues import ( + AbciDialogue as BaseAbciDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + AbciDialogues as BaseAbciDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + ContractApiDialogue as BaseContractApiDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + ContractApiDialogues as BaseContractApiDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + HttpDialogue as BaseHttpDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + HttpDialogues as BaseHttpDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + LedgerApiDialogue as BaseLedgerApiDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + LedgerApiDialogues as BaseLedgerApiDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + SigningDialogue as BaseSigningDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + SigningDialogues as BaseSigningDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + TendermintDialogue as BaseTendermintDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + TendermintDialogues as BaseTendermintDialogues, +) + + +AbciDialogue = BaseAbciDialogue +AbciDialogues = BaseAbciDialogues + + +HttpDialogue = BaseHttpDialogue +HttpDialogues = BaseHttpDialogues + + +SigningDialogue = BaseSigningDialogue +SigningDialogues = BaseSigningDialogues + + +LedgerApiDialogue = BaseLedgerApiDialogue +LedgerApiDialogues = BaseLedgerApiDialogues + + +ContractApiDialogue = BaseContractApiDialogue +ContractApiDialogues = BaseContractApiDialogues + + +TendermintDialogue = BaseTendermintDialogue +TendermintDialogues = BaseTendermintDialogues diff --git a/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/handlers.py b/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/handlers.py new file mode 100644 index 0000000..fc0edb2 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/handlers.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the handlers for the skill of DummyAbciApp.""" + +from packages.valory.skills.abstract_round_abci.handlers import ( + ABCIRoundHandler as BaseABCIRoundHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + ContractApiHandler as BaseContractApiHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + HttpHandler as BaseHttpHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + LedgerApiHandler as BaseLedgerApiHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + SigningHandler as BaseSigningHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + TendermintHandler as BaseTendermintHandler, +) + + +ABCIRoundHandler = BaseABCIRoundHandler +HttpHandler = BaseHttpHandler +SigningHandler = BaseSigningHandler +LedgerApiHandler = BaseLedgerApiHandler +ContractApiHandler = BaseContractApiHandler +TendermintHandler = BaseTendermintHandler diff --git a/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/models.py b/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/models.py new file mode 100644 index 0000000..5fba375 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/models.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2022-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the shared state for the abci skill of DummyAbciApp.""" + +from packages.valory.skills.abstract_round_abci.models import ApiSpecs, BaseParams +from packages.valory.skills.abstract_round_abci.models import ( + BenchmarkTool as BaseBenchmarkTool, +) +from packages.valory.skills.abstract_round_abci.models import Requests as BaseRequests +from packages.valory.skills.abstract_round_abci.models import ( + SharedState as BaseSharedState, +) +from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci.rounds import ( + DummyAbciApp, +) + + +class SharedState(BaseSharedState): + """Keep the current shared state of the skill.""" + + abci_app_cls = DummyAbciApp + + +Params = BaseParams +Requests = BaseRequests +BenchmarkTool = BaseBenchmarkTool +RandomnessApi = ApiSpecs diff --git a/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/payloads.py b/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/payloads.py new file mode 100644 index 0000000..bc4308b --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/payloads.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2022-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the transaction payloads of the DummyAbciApp.""" + +from dataclasses import dataclass + +from packages.valory.skills.abstract_round_abci.base import BaseTxPayload + + +@dataclass(frozen=True) +class DummyStartingPayload(BaseTxPayload): + """Represent a transaction payload for the DummyStartingRound.""" + + content: str + + +@dataclass(frozen=True) +class DummyRandomnessPayload(BaseTxPayload): + """Represent a transaction payload for the DummyRandomnessRound.""" + + round_id: int + randomness: str + + +@dataclass(frozen=True) +class DummyKeeperSelectionPayload(BaseTxPayload): + """Represent a transaction payload for the DummyKeeperSelectionRound.""" + + keepers: str + + +@dataclass(frozen=True) +class DummyFinalPayload(BaseTxPayload): + """Represent a transaction payload for the DummyFinalRound.""" + + content: bool diff --git a/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/rounds.py b/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/rounds.py new file mode 100644 index 0000000..b3ee3bc --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/rounds.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2022-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains the rounds of DummyAbciApp.""" + +from abc import ABC +from enum import Enum +from typing import FrozenSet, Optional, Set, Tuple, cast + +from packages.valory.skills.abstract_round_abci.base import ( + AbciApp, + AbciAppTransitionFunction, + AbstractRound, + AppState, + BaseSynchronizedData, + CollectSameUntilAllRound, + CollectSameUntilThresholdRound, + EventToTimeout, + OnlyKeeperSendsRound, +) +from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci.payloads import ( + DummyFinalPayload, + DummyKeeperSelectionPayload, + DummyRandomnessPayload, + DummyStartingPayload, +) + + +class Event(Enum): + """DummyAbciApp Events""" + + ROUND_TIMEOUT = "round_timeout" + NO_MAJORITY = "no_majority" + DONE = "done" + + +class SynchronizedData(BaseSynchronizedData): + """ + Class to represent the synchronized data. + + This data is replicated by the tendermint application. + """ + + +class DummyMixinRound(AbstractRound, ABC): + """DummyMixinRound""" + + done_event = Event.DONE + no_majority_event = Event.NO_MAJORITY + + @property + def synchronized_data(self) -> SynchronizedData: + """Return the synchronized data.""" + return cast(SynchronizedData, self._synchronized_data) + + +class DummyStartingRound(CollectSameUntilAllRound, DummyMixinRound): + """DummyStartingRound""" + + round_id: str = "dummy_starting" + payload_class = DummyStartingPayload + payload_attribute: str = "dummy_starting" + synchronized_data_class = SynchronizedData + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: + """Process the end of the block.""" + + if self.collection_threshold_reached: + synchronized_data = self.synchronized_data.update( + participants=tuple(sorted(self.collection)), + synchronized_data_class=SynchronizedData, + ) + return synchronized_data, Event.DONE + return None + + +class DummyRandomnessRound(CollectSameUntilThresholdRound, DummyMixinRound): + """DummyRandomnessRound""" + + round_id: str = "dummy_randomness" + payload_class = DummyRandomnessPayload + payload_attribute: str = "dummy_randomness" + collection_key = "participant_to_randomness" + selection_key = "most_voted_randomness" + synchronized_data_class = SynchronizedData + + +class DummyKeeperSelectionRound(CollectSameUntilThresholdRound, DummyMixinRound): + """DummyKeeperSelectionRound""" + + round_id: str = "dummy_keeper_selection" + payload_class = DummyKeeperSelectionPayload + payload_attribute: str = "dummy_keeper_selection" + collection_key = "participant_to_keeper" + selection_key = "most_voted_keeper" + synchronized_data_class = SynchronizedData + + +class DummyFinalRound(OnlyKeeperSendsRound, DummyMixinRound): + """DummyFinalRound""" + + round_id: str = "dummy_final" + payload_class = DummyFinalPayload + payload_attribute: str = "dummy_final" + synchronized_data_class = SynchronizedData + + +class DummyAbciApp(AbciApp[Event]): + """DummyAbciApp""" + + initial_round_cls: AppState = DummyStartingRound + transition_function: AbciAppTransitionFunction = { + DummyStartingRound: { + Event.DONE: DummyRandomnessRound, + Event.ROUND_TIMEOUT: DummyStartingRound, + Event.NO_MAJORITY: DummyStartingRound, + }, + DummyRandomnessRound: { + Event.DONE: DummyKeeperSelectionRound, + Event.ROUND_TIMEOUT: DummyRandomnessRound, + Event.NO_MAJORITY: DummyRandomnessRound, + }, + DummyKeeperSelectionRound: { + Event.DONE: DummyFinalRound, + Event.ROUND_TIMEOUT: DummyKeeperSelectionRound, + Event.NO_MAJORITY: DummyKeeperSelectionRound, + }, + DummyFinalRound: { + Event.DONE: DummyStartingRound, + Event.ROUND_TIMEOUT: DummyFinalRound, + Event.NO_MAJORITY: DummyFinalRound, + }, + } + final_states: Set[AppState] = set() + event_to_timeout: EventToTimeout = {} + cross_period_persisted_keys: FrozenSet[str] = frozenset() diff --git a/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/skill.yaml b/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/skill.yaml new file mode 100644 index 0000000..4ac35e9 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/skill.yaml @@ -0,0 +1,148 @@ +name: dummy_abci +author: dummy +version: 0.1.0 +type: skill +description: The scaffold skill is a scaffold for your own skill implementation. +license: Apache-2.0 +aea_version: '>=1.0.0, <2.0.0' +fingerprint: + __init__.py: bafybeielxta5wbywv6cb2p65phk73zeodroif7imk33qc7sxgxrcelr62y + behaviours.py: bafybeicwwlex4z5ro6hrw5cdwxyp5742klcqsjwcn423wgg3jk6xclzwvi + dialogues.py: bafybeiaswubmqa7trhajbjn34okmpftk2sehsqrjg7znzrrd7j32xzx4vq + handlers.py: bafybeifik3ftljs63u7nm4gadxpqbcvqj53p7qftzzzfto3ioad57k3x3u + models.py: bafybeif4lp5i6an4z4kkquh3x3ttsvfctvsu5excmxahjywbbbo7g3js5y + payloads.py: bafybeidllmzsctg3m5jhawbt3kzk6ieodtvgwklrquqehtqtzzwhkxxg4a + rounds.py: bafybeiab5q6pzh544uuc672hksh4rv6a74dunt4ztdnqo4gw3hnzd452ti + tests/__init__.py: bafybeiaxqzwmh36bhquqztcyrkxjjkz5cctseqetglrwdezgnkjrtg2654 + tests/test_behaviours.py: bafybeich3uo67gdbxrxsivlrxfgpfuixupl6qtotxxp2qqpyqnck4i67eu + tests/test_dialogues.py: bafybeice2v4xnsjhhlnpbejnvpory5spmrewwcfsefzqzq3uhfyya5hypm + tests/test_handlers.py: bafybeidrfumnc743qh5s2ahf5rxu3rzrroygxwpbqa7jtqxg5kirjzedjm + tests/test_models.py: bafybeifuxjmpv3eet2zn7vc5btprakueqlk2ybc2fxgzbtiho5wdslkeb4 + tests/test_payloads.py: bafybeicvbisfw5prv6jw3is3vw6gehsplt3teyeo6dbeh37xazh4izeyhq + tests/test_rounds.py: bafybeihjepr2hubbgmb7jkeldbam3zmsgwn6nffif7zp4etqlv2bt5rsxy +fingerprint_ignore_patterns: [] +connections: [] +contracts: [] +protocols: [] +skills: +- valory/abstract_round_abci:0.1.0:bafybeifjnk2v3cw233ke5qhakurvdsex64c5runjctclrh7y64tyh7uqrq +behaviours: + main: + args: {} + class_name: DummyRoundBehaviour +handlers: + abci: + args: {} + class_name: ABCIRoundHandler + contract_api: + args: {} + class_name: ContractApiHandler + http: + args: {} + class_name: HttpHandler + ledger_api: + args: {} + class_name: LedgerApiHandler + signing: + args: {} + class_name: SigningHandler + tendermint: + args: {} + class_name: TendermintHandler +models: + abci_dialogues: + args: {} + class_name: AbciDialogues + benchmark_tool: + args: + log_dir: /logs + class_name: BenchmarkTool + contract_api_dialogues: + args: {} + class_name: ContractApiDialogues + http_dialogues: + args: {} + class_name: HttpDialogues + ledger_api_dialogues: + args: {} + class_name: LedgerApiDialogues + params: + args: + cleanup_history_depth: 1 + cleanup_history_depth_current: null + drand_public_key: 868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31 + genesis_config: + chain_id: chain-c4daS1 + consensus_params: + block: + max_bytes: '22020096' + max_gas: '-1' + time_iota_ms: '1000' + evidence: + max_age_duration: '172800000000000' + max_age_num_blocks: '100000' + max_bytes: '1048576' + validator: + pub_key_types: + - ed25519 + version: {} + genesis_time: '2022-05-20T16:00:21.735122717Z' + voting_power: '10' + keeper_timeout: 30.0 + max_healthcheck: 120 + reset_pause_duration: 10 + on_chain_service_id: null + reset_tendermint_after: 2 + retry_attempts: 400 + retry_timeout: 3 + round_timeout_seconds: 30.0 + service_id: dummy + service_registry_address: null + setup: + all_participants: + - '0x0000000000000000000000000000000000000000' + safe_contract_address: '0x0000000000000000000000000000000000000000' + consensus_threshold: null + sleep_time: 1 + tendermint_check_sleep_delay: 3 + tendermint_com_url: http://localhost:8080 + tendermint_max_retries: 5 + tendermint_url: http://localhost:26657 + request_timeout: 10.0 + request_retry_delay: 1.0 + tx_timeout: 10.0 + max_attempts: 10 + share_tm_config_on_startup: false + tendermint_p2p_url: localhost:26656 + use_termination: false + use_slashing: false + slash_cooldown_hours: 3 + slash_threshold_amount: 10_000_000_000_000_000 + light_slash_unit_amount: 5_000_000_000_000_000 + serious_slash_unit_amount: 8_000_000_000_000_000 + class_name: Params + randomness_api: + args: + api_id: cloudflare + headers: {} + method: GET + parameters: {} + response_key: null + response_type: dict + retries: 5 + url: https://drand.cloudflare.com/public/latest + class_name: RandomnessApi + requests: + args: {} + class_name: Requests + signing_dialogues: + args: {} + class_name: SigningDialogues + state: + args: {} + class_name: SharedState + tendermint_dialogues: + args: {} + class_name: TendermintDialogues +dependencies: {} +is_abstract: false diff --git a/packages/valory/skills/abstract_round_abci/tests/test_abci_app_chain.py b/packages/valory/skills/abstract_round_abci/tests/test_abci_app_chain.py new file mode 100644 index 0000000..4a95580 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/test_abci_app_chain.py @@ -0,0 +1,508 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test the abci_app_chain.py module of the skill.""" + +# pylint: skip-file + +import logging +from typing import Dict, Set, Tuple, Type +from unittest.mock import MagicMock + +import pytest +from _pytest.logging import LogCaptureFixture +from aea.exceptions import AEAEnforceError + +from packages.valory.skills.abstract_round_abci.abci_app_chain import ( + AbciAppTransitionMapping, + chain, +) +from packages.valory.skills.abstract_round_abci.base import ( + AbciApp, + AbciAppDB, + AbstractRound, + AppState, + BaseSynchronizedData, + BaseTxPayload, + DegenerateRound, +) + + +def make_round_class(name: str, bases: Tuple = (AbstractRound,)) -> Type[AbstractRound]: + """Make a round class.""" + new_round_cls = type( + name, + bases, + { + "synchronized_data_class": MagicMock(), + "payload_class": MagicMock(), + "payload_attribute": MagicMock(), + }, + ) + setattr(new_round_cls, "round_id", name) # noqa: B010 + assert issubclass(new_round_cls, AbstractRound) # nosec + return new_round_cls + + +class TestAbciAppChaining: + """Test chaning of AbciApps.""" + + def setup(self) -> None: + """Setup test.""" + self.round_1a = make_round_class("round_1a") + self.round_1b = make_round_class("round_1b") + self.round_1b_dupe = make_round_class("round_1b") # duplicated round id + self.round_1c = make_round_class("round_1c", (DegenerateRound,)) + + self.round_2a = make_round_class("round_2a") + self.round_2b = make_round_class("round_2b") + self.round_2c = make_round_class("round_2c", (DegenerateRound,)) + self.round_2d = make_round_class("round_2d") + + self.round_3a = make_round_class("round_3a") + self.round_3b = make_round_class("round_3b") + self.round_3c = make_round_class("round_3c", (DegenerateRound,)) + + self.key_1 = "1" + self.key_2 = "2" + self.key_3 = "3" + + self.event_1a = "event_1a" + self.event_1b = "event_1b" + self.event_1c = "event_1c" + self.event_timeout1 = "timeout_1" + + self.event_2a = "event_2a" + self.event_2b = "event_2b" + self.event_2c = "event_2c" + self.event_timeout2 = "timeout_2" + + self.event_3a = "event_3a" + self.event_3b = "event_3b" + self.event_3c = "event_3c" + self.event_timeout3 = "timeout_3" + + self.timeout1 = 10.0 + self.timeout2 = 15.0 + self.timeout3 = 20.0 + + self.cross_period_persisted_keys_1 = frozenset({"1", "2"}) + self.cross_period_persisted_keys_2 = frozenset({"2", "3"}) + + class AbciApp1(AbciApp): + initial_round_cls = self.round_1a + transition_function = { + self.round_1a: { + self.event_timeout1: self.round_1a, + self.event_1b: self.round_1b, + }, + self.round_1b: { + self.event_1a: self.round_1a, + self.event_1c: self.round_1c, + }, + self.round_1c: {}, + } + final_states = {self.round_1c} + event_to_timeout = {self.event_timeout1: self.timeout1} + db_pre_conditions: Dict[AppState, Set[str]] = {self.round_1a: set()} + db_post_conditions: Dict[AppState, Set[str]] = {self.round_1c: {self.key_1}} + cross_period_persisted_keys = self.cross_period_persisted_keys_1 + + self.app1_class = AbciApp1 + + class AbciApp2(AbciApp): + initial_round_cls = self.round_2a + transition_function = { + self.round_2a: { + self.event_timeout2: self.round_2a, + self.event_2b: self.round_2b, + }, + self.round_2b: { + self.event_2a: self.round_2a, + self.event_2c: self.round_2c, + }, + self.round_2c: {}, + } + final_states = {self.round_2c} + event_to_timeout = {self.event_timeout2: self.timeout2} + db_pre_conditions: Dict[AppState, Set[str]] = {self.round_2a: {self.key_1}} + db_post_conditions: Dict[AppState, Set[str]] = {self.round_2c: {self.key_2}} + cross_period_persisted_keys = self.cross_period_persisted_keys_2 + + self.app2_class = AbciApp2 + + class AbciApp3(AbciApp): + initial_round_cls = self.round_3a + transition_function = { + self.round_3a: { + self.event_timeout3: self.round_3a, + self.event_3b: self.round_3b, + }, + self.round_3b: { + self.event_3a: self.round_3a, + self.event_3c: self.round_3c, + self.event_1a: self.round_3a, # duplicated event + }, + self.round_3c: {}, + } + final_states = {self.round_3c} + event_to_timeout = {self.event_timeout3: self.timeout3} + db_pre_conditions: Dict[AppState, Set[str]] = { + self.round_3a: {self.key_1, self.key_2} + } + db_post_conditions: Dict[AppState, Set[str]] = {self.round_3c: {self.key_3}} + + self.app3_class = AbciApp3 + + class AbciApp3Dupe(AbciApp): + initial_round_cls = self.round_3a + transition_function = { + self.round_3a: { + self.event_timeout3: self.round_3a, + self.event_3b: self.round_3b, + }, + self.round_1b_dupe: { # duplucated round id + self.event_3a: self.round_3a, + self.event_3c: self.round_3c, + self.event_1a: self.round_3a, # duplicated event + }, + self.round_3c: {}, + } + final_states = {self.round_3c} + event_to_timeout = {self.event_timeout3: self.timeout3} + db_post_conditions: Dict[AppState, Set[str]] = {self.round_3c: set()} + + self.app3_class_dupe = AbciApp3Dupe + + class AbciApp2Faulty1(AbciApp): + initial_round_cls = self.round_2a + transition_function = { + self.round_2a: { + self.event_timeout2: self.round_2a, + self.event_2b: self.round_2b, + }, + self.round_2b: { + self.event_2a: self.round_2a, + self.event_2c: self.round_2c, + }, + self.round_2c: {}, + } + final_states = {self.round_2c} + event_to_timeout = {self.event_timeout1: self.timeout2} + db_pre_conditions: Dict[AppState, Set[str]] = {self.round_2a: {self.key_1}} + db_post_conditions: Dict[AppState, Set[str]] = {self.round_2c: {self.key_2}} + + self.app2_class_faulty1 = AbciApp2Faulty1 + + def test_chain_two(self) -> None: + """Test the AbciApp chain function.""" + + abci_app_transition_mapping: AbciAppTransitionMapping = { + self.round_1c: self.round_2a, + self.round_2c: self.round_1a, + } + + ComposedAbciApp = chain( + (self.app1_class, self.app2_class), abci_app_transition_mapping + ) + + assert ComposedAbciApp.initial_round_cls == self.round_1a + assert ComposedAbciApp.transition_function == { + self.round_1a: { + self.event_timeout1: self.round_1a, + self.event_1b: self.round_1b, + }, + self.round_1b: { + self.event_1a: self.round_1a, + self.event_1c: self.round_2a, + }, + self.round_2a: { + self.event_timeout2: self.round_2a, + self.event_2b: self.round_2b, + }, + self.round_2b: { + self.event_2a: self.round_2a, + self.event_2c: self.round_1a, + }, + } + assert ComposedAbciApp.final_states == set() + assert ComposedAbciApp.event_to_timeout == { + self.event_timeout1: self.timeout1, + self.event_timeout2: self.timeout2, + } + assert ( + ComposedAbciApp.cross_period_persisted_keys + == self.cross_period_persisted_keys_1.union( + self.cross_period_persisted_keys_2 + ) + ) + + def test_chain_three(self) -> None: + """Test the AbciApp chain function.""" + + abci_app_transition_mapping: AbciAppTransitionMapping = { + self.round_1c: self.round_2a, + self.round_2c: self.round_3a, + } + + ComposedAbciApp = chain( + (self.app1_class, self.app2_class, self.app3_class), + abci_app_transition_mapping, + ) + + assert ComposedAbciApp.initial_round_cls == self.round_1a + assert ComposedAbciApp.transition_function == { + self.round_1a: { + self.event_timeout1: self.round_1a, + self.event_1b: self.round_1b, + }, + self.round_1b: { + self.event_1a: self.round_1a, + self.event_1c: self.round_2a, + }, + self.round_2a: { + self.event_timeout2: self.round_2a, + self.event_2b: self.round_2b, + }, + self.round_2b: { + self.event_2a: self.round_2a, + self.event_2c: self.round_3a, + }, + self.round_3a: { + self.event_timeout3: self.round_3a, + self.event_3b: self.round_3b, + }, + self.round_3b: { + self.event_3a: self.round_3a, + self.event_3c: self.round_3c, + self.event_1a: self.round_3a, + }, + self.round_3c: {}, + } + assert ComposedAbciApp.final_states == {self.round_3c} + assert ComposedAbciApp.event_to_timeout == { + self.event_timeout1: self.timeout1, + self.event_timeout2: self.timeout2, + self.event_timeout3: self.timeout3, + } + + def test_chain_two_negative_timeouts(self) -> None: + """Test the AbciApp chain function.""" + + abci_app_transition_mapping: AbciAppTransitionMapping = { + self.round_1c: self.round_2a, + self.round_2c: self.round_1a, + } + + with pytest.raises( + ValueError, match="but it is already defined in a prior app with timeout" + ): + _ = chain( + (self.app1_class, self.app2_class_faulty1), abci_app_transition_mapping + ) + + def test_chain_two_negative_mapping_initial_states(self) -> None: + """Test the AbciApp chain function.""" + + abci_app_transition_mapping: AbciAppTransitionMapping = { + self.round_1c: self.round_2b, + self.round_2c: self.round_1a, + } + + with pytest.raises(ValueError, match="Found non-initial state"): + _ = chain((self.app1_class, self.app2_class), abci_app_transition_mapping) + + def test_chain_two_negative_mapping_final_states(self) -> None: + """Test the AbciApp chain function.""" + + abci_app_transition_mapping: AbciAppTransitionMapping = { + self.round_1c: self.round_2a, + self.round_2b: self.round_1a, + } + + with pytest.raises(ValueError, match="Found non-final state"): + _ = chain((self.app1_class, self.app2_class), abci_app_transition_mapping) + + def test_chain_two_dupe(self) -> None: + """Test the AbciApp chain function.""" + + abci_app_transition_mapping: AbciAppTransitionMapping = { + self.round_1c: self.round_2a, + self.round_2c: self.round_1a, + } + with pytest.raises( + AEAEnforceError, + match=r"round ids in common between abci apps are not allowed.*", + ): + chain((self.app1_class, self.app3_class_dupe), abci_app_transition_mapping) + + def test_chain_with_abstract_abci_app_fails(self) -> None: + """Test chaining with an abstract AbciApp fails.""" + self.app2_class._is_abstract = False + self.app3_class._is_abstract = False + with pytest.raises( + AEAEnforceError, + match=r"found non-abstract AbciApp during chaining: \['AbciApp2', 'AbciApp3'\]", + ): + abci_app_transition_mapping: AbciAppTransitionMapping = { + self.round_1c: self.round_2a, + self.round_2c: self.round_3a, + } + chain( + (self.app1_class, self.app2_class, self.app3_class), + abci_app_transition_mapping, + ) + + def test_synchronized_data_type(self, caplog: LogCaptureFixture) -> None: + """Test synchronized data type""" + + abci_app_transition_mapping: AbciAppTransitionMapping = { + self.round_1c: self.round_2a, + self.round_2c: self.round_1a, + } + + sentinel_app1 = object() + sentinel_app2 = object() + + def make_sync_data(sentinel: object) -> Type: + class SynchronizedData(BaseSynchronizedData): + @property + def dummy_attr(self) -> object: + return sentinel + + return SynchronizedData + + def make_concrete(round_cls: Type[AbstractRound]) -> Type[AbstractRound]: + class ConcreteRound(round_cls): # type: ignore + def check_payload(self, payload: BaseTxPayload) -> None: + pass + + def process_payload(self, payload: BaseTxPayload) -> None: + pass + + def end_block(self) -> None: + pass + + payload_class = None + + return ConcreteRound + + sync_data_cls_app1 = make_sync_data(sentinel_app1) + sync_data_cls_app2 = make_sync_data(sentinel_app2) + + # don't need to mock all of this since setup creates these classes + for abci_app_cls, sync_data_cls in ( + (self.app1_class, sync_data_cls_app1), + (self.app2_class, sync_data_cls_app2), + ): + synchronized_data = sync_data_cls(db=AbciAppDB(setup_data={})) + abci_app = abci_app_cls(synchronized_data, logging.getLogger(), MagicMock()) + for r in abci_app_cls.get_all_rounds(): + r.synchronized_data_class = sync_data_cls + + abci_app_cls = chain( + (self.app1_class, self.app2_class), abci_app_transition_mapping + ) + synchronized_data = sync_data_cls_app2(db=AbciAppDB(setup_data={})) + abci_app = abci_app_cls(synchronized_data, logging.getLogger(), MagicMock()) + + assert abci_app.initial_round_cls == self.round_1a + assert isinstance(abci_app.synchronized_data, sync_data_cls_app1) + assert abci_app.synchronized_data.dummy_attr == sentinel_app1 + + app2_classes = self.app2_class.get_all_rounds() + for round_ in sorted(abci_app.get_all_rounds(), key=str): + abci_app._round_results.append(abci_app.synchronized_data) + abci_app.schedule_round(make_concrete(round_)) + expected_cls = (sync_data_cls_app1, sync_data_cls_app2)[ + round_ in app2_classes + ] + assert isinstance(abci_app.synchronized_data, expected_cls) + expected_sentinel = (sentinel_app1, sentinel_app2)[round_ in app2_classes] + assert abci_app.synchronized_data.dummy_attr == expected_sentinel + + def test_precondition_for_next_app_missing_raises( + self, caplog: LogCaptureFixture + ) -> None: + """Test that when precondition for next AbciApp is missing an error is raised""" + + class AbciApp1(AbciApp): + initial_round_cls = self.round_1a + transition_function = { + self.round_1a: { + self.event_timeout1: self.round_1a, + self.event_1b: self.round_1b, + }, + self.round_1b: { + self.event_1a: self.round_1a, + self.event_1c: self.round_1c, + }, + self.round_1c: {}, + } + final_states = {self.round_1c} + event_to_timeout = {self.event_timeout1: self.timeout1} + db_pre_conditions: Dict[AppState, Set[str]] = {self.round_1a: set()} + db_post_conditions: Dict[AppState, Set[str]] = {self.round_1c: set()} + cross_period_persisted_keys = self.cross_period_persisted_keys_1 + + abci_app_transition_mapping: AbciAppTransitionMapping = { + self.round_1c: self.round_2a, + self.round_2c: self.round_1a, + } + + expected = "Pre conditions '.*' of app '.*' not a post condition of app '.*' or any preceding app in path .*." + with pytest.raises(ValueError, match=expected): + chain( + ( + AbciApp1, + self.app2_class, + ), + abci_app_transition_mapping, + ) + + def test_precondition_app_missing_raises(self, caplog: LogCaptureFixture) -> None: + """Test that missing precondition specification for next AbciApp is missing an error is raised""" + + class AbciApp2(AbciApp): + initial_round_cls = self.round_2a + transition_function = { + self.round_2a: { + self.event_timeout2: self.round_2a, + self.event_2b: self.round_2b, + }, + self.round_2b: { + self.event_2a: self.round_2a, + self.event_2c: self.round_2c, + }, + self.round_2c: {}, + } + final_states = {self.round_2c} + event_to_timeout = {self.event_timeout2: self.timeout2} + db_pre_conditions: Dict[AppState, Set[str]] = {} + db_post_conditions: Dict[AppState, Set[str]] = {self.round_2c: {self.key_2}} + cross_period_persisted_keys = self.cross_period_persisted_keys_2 + + abci_app_transition_mapping: AbciAppTransitionMapping = { + self.round_1c: self.round_2a, + self.round_2c: self.round_1a, + } + + expected = "No pre-conditions have been set for .*! You need to explicitly specify them as empty if there are no pre-conditions for this FSM." + with pytest.raises(ValueError, match=expected): + chain((self.app1_class, AbciApp2), abci_app_transition_mapping) diff --git a/packages/valory/skills/abstract_round_abci/tests/test_base.py b/packages/valory/skills/abstract_round_abci/tests/test_base.py new file mode 100644 index 0000000..c5ff0f4 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/test_base.py @@ -0,0 +1,3420 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test the base.py module of the skill.""" + +import dataclasses +import datetime +import json +import logging +import re +import shutil +from abc import ABC +from calendar import timegm +from collections import deque +from contextlib import suppress +from copy import copy, deepcopy +from dataclasses import dataclass +from enum import Enum +from pathlib import Path +from time import sleep +from typing import ( + Any, + Callable, + Deque, + Dict, + FrozenSet, + Generator, + List, + Optional, + Set, + Tuple, + Type, + cast, +) +from unittest import mock +from unittest.mock import MagicMock + +import pytest +from _pytest.logging import LogCaptureFixture +from aea.exceptions import AEAEnforceError +from aea_ledger_ethereum import EthereumCrypto +from hypothesis import HealthCheck, given, settings +from hypothesis.strategies import ( + DrawFn, + binary, + booleans, + builds, + composite, + data, + datetimes, + dictionaries, + floats, + integers, + just, + lists, + none, + one_of, + sampled_from, + text, +) + +import packages.valory.skills.abstract_round_abci.base as abci_base +from packages.valory.connections.abci.connection import MAX_READ_IN_BYTES +from packages.valory.protocols.abci.custom_types import ( + Evidence, + EvidenceType, + Evidences, + LastCommitInfo, + Timestamp, + Validator, + VoteInfo, +) +from packages.valory.skills.abstract_round_abci.base import ( + ABCIAppException, + ABCIAppInternalError, + AbciApp, + AbciAppDB, + AbciAppTransitionFunction, + AbstractRound, + AbstractRoundInternalError, + AddBlockError, + AppState, + AvailabilityWindow, + BaseSynchronizedData, + BaseTxPayload, + Block, + BlockBuilder, + Blockchain, + CollectionRound, + EventType, + LateArrivingTransaction, + OffenceStatus, + OffenseStatusDecoder, + OffenseStatusEncoder, + OffenseType, + RoundSequence, + SignatureNotValidError, + SlashingNotConfiguredError, + Timeouts, + Transaction, + TransactionTypeNotRecognizedError, + _MetaAbciApp, + _MetaAbstractRound, + _MetaPayload, + get_name, + light_offences, + serious_offences, +) +from packages.valory.skills.abstract_round_abci.test_tools.abci_app import ( + AbciAppTest, + ConcreteBackgroundRound, + ConcreteBackgroundSlashingRound, + ConcreteEvents, + ConcreteRoundA, + ConcreteRoundB, + ConcreteRoundC, + ConcreteSlashingRoundA, + ConcreteTerminationRoundA, + ConcreteTerminationRoundB, + ConcreteTerminationRoundC, + SlashingAppTest, + TerminationAppTest, +) +from packages.valory.skills.abstract_round_abci.test_tools.rounds import ( + BaseRoundTestClass, + get_participants, +) +from packages.valory.skills.abstract_round_abci.tests.conftest import profile_name + + +# pylint: skip-file + + +settings.load_profile(profile_name) + + +PACKAGE_DIR = Path(__file__).parent.parent + + +DUMMY_CONCRETE_BACKGROUND_PAYLOAD = ConcreteBackgroundRound.payload_class( + sender="sender" +) + + +@pytest.fixture(scope="session", autouse=True) +def hypothesis_cleanup() -> Generator: + """Fixture to remove hypothesis directory after tests.""" + yield + hypothesis_dir = PACKAGE_DIR / ".hypothesis" + if hypothesis_dir.exists(): + with suppress(OSError, PermissionError): + shutil.rmtree(hypothesis_dir) + + +class BasePayload(BaseTxPayload, ABC): + """Base payload class for testing.""" + + +@dataclass(frozen=True) +class PayloadA(BasePayload): + """Payload class for payload type 'A'.""" + + +@dataclass(frozen=True) +class PayloadB(BasePayload): + """Payload class for payload type 'B'.""" + + +@dataclass(frozen=True) +class PayloadC(BasePayload): + """Payload class for payload type 'C'.""" + + +@dataclass(frozen=True) +class PayloadD(BasePayload): + """Payload class for payload type 'D'.""" + + +@dataclass(frozen=True) +class DummyPayload(BasePayload): + """Dummy payload class.""" + + dummy_attribute: int + + +@dataclass(frozen=True) +class TooBigPayload(BaseTxPayload): + """Base payload class for testing.""" + + dummy_field: str = "0" * 10**7 + + +class ObjectImitator: + """For custom __eq__ implementation testing""" + + def __init__(self, other: Any): + """Copying references to class attr, and instance attr""" + + for attr, value in vars(other.__class__).items(): + if not attr.startswith("__") and not attr.endswith("__"): + setattr(self.__class__, attr, value) + + self.__dict__ = other.__dict__ + + +def test_base_tx_payload() -> None: + """Test BaseTxPayload.""" + + payload = PayloadA(sender="sender") + object.__setattr__(payload, "round_count", 9) + new_payload = payload.with_new_id() + + assert not payload == new_payload + payload_data, new_payload_data = payload.json, new_payload.json + assert not payload_data.pop("id_") == new_payload_data.pop("id_") + assert payload_data == new_payload_data + with pytest.raises(dataclasses.FrozenInstanceError): + payload.round_count = 1 # type: ignore + object.__setattr__(payload, "round_count", 1) + assert payload.round_count == 1 + assert type(hash(payload)) == int + + +def test_meta_round_abstract_round_when_instance_not_subclass_of_abstract_round() -> ( + None +): + """Test instantiation of meta class when instance not a subclass of abstract round.""" + + class MyAbstractRound(metaclass=_MetaAbstractRound): + pass + + +def test_abstract_round_instantiation_without_attributes_raises_error() -> None: + """Test that definition of concrete subclass of AbstractRound without attributes raises error.""" + with pytest.raises(AbstractRoundInternalError): + + class MyRoundBehaviour(AbstractRound): + pass + + with pytest.raises(AbstractRoundInternalError): + + class MyRoundBehaviourB(AbstractRound): + synchronized_data_class = MagicMock() + + +class TestTransactions: + """Test Transactions class.""" + + def setup(self) -> None: + """Set up the test.""" + self.old_value = copy(_MetaPayload.registry) + + def test_encode_decode(self) -> None: + """Test encoding and decoding of payloads.""" + sender = "sender" + + expected_payload = PayloadA(sender=sender) + actual_payload = PayloadA.decode(expected_payload.encode()) + assert expected_payload == actual_payload + + expected_payload_ = PayloadB(sender=sender) + actual_payload_ = PayloadB.decode(expected_payload_.encode()) + assert expected_payload_ == actual_payload_ + + expected_payload__ = PayloadC(sender=sender) + actual_payload__ = PayloadC.decode(expected_payload__.encode()) + assert expected_payload__ == actual_payload__ + + expected_payload___ = PayloadD(sender=sender) + actual_payload___ = PayloadD.decode(expected_payload___.encode()) + assert expected_payload___ == actual_payload___ + + def test_encode_decode_transaction(self) -> None: + """Test encode/decode of a transaction.""" + sender = "sender" + signature = "signature" + payload = PayloadA(sender) + expected = Transaction(payload, signature) + actual = expected.decode(expected.encode()) + assert expected == actual + + def test_encode_too_big_payload(self) -> None: + """Test encode of a too big payload.""" + sender = "sender" + payload = TooBigPayload(sender) + with pytest.raises( + ValueError, + match=f"{type(payload)} must be smaller than {MAX_READ_IN_BYTES} bytes", + ): + payload.encode() + + def test_encode_too_big_transaction(self) -> None: + """Test encode of a too big transaction.""" + sender = "sender" + signature = "signature" + payload = TooBigPayload(sender) + tx = Transaction(payload, signature) + with pytest.raises( + ValueError, + match=f"Transaction must be smaller than {MAX_READ_IN_BYTES} bytes", + ): + tx.encode() + + def test_sign_verify_transaction(self) -> None: + """Test sign/verify transaction.""" + crypto = EthereumCrypto() + sender = crypto.address + payload = PayloadA(sender) + payload_bytes = payload.encode() + signature = crypto.sign_message(payload_bytes) + transaction = Transaction(payload, signature) + transaction.verify(crypto.identifier) + + def test_payload_not_equal_lookalike(self) -> None: + """Test payload __eq__ reflection via NotImplemented""" + payload = PayloadA(sender="sender") + lookalike = ObjectImitator(payload) + assert not payload == lookalike + + def test_transaction_not_equal_lookalike(self) -> None: + """Test transaction __eq__ reflection via NotImplemented""" + payload = PayloadA(sender="sender") + transaction = Transaction(payload, signature="signature") + lookalike = ObjectImitator(transaction) + assert not transaction == lookalike + + def teardown(self) -> None: + """Tear down the test.""" + _MetaPayload.registry = self.old_value + + +@mock.patch( + "aea.crypto.ledger_apis.LedgerApis.recover_message", return_value={"wrong_sender"} +) +def test_verify_transaction_negative_case(*_mocks: Any) -> None: + """Test verify() of transaction, negative case.""" + transaction = Transaction(MagicMock(sender="right_sender", json={}), "") + with pytest.raises( + SignatureNotValidError, match="Signature not valid on transaction: .*" + ): + transaction.verify("") + + +@dataclass(frozen=True) +class SomeClass(BaseTxPayload): + """Test class.""" + + content: Dict + + +@given( + dictionaries( + keys=text(), + values=one_of(floats(allow_nan=False, allow_infinity=False), booleans()), + ) +) +def test_payload_serializer_is_deterministic(obj: Any) -> None: + """Test that 'DictProtobufStructSerializer' is deterministic.""" + obj_ = SomeClass(sender="", content=obj) + obj_bytes = obj_.encode() + assert obj_ == BaseTxPayload.decode(obj_bytes) + + +def test_initialize_block() -> None: + """Test instantiation of a Block instance.""" + block = Block(MagicMock(), []) + assert block.transactions == tuple() + + +class TestBlockchain: + """Test a blockchain object.""" + + def setup(self) -> None: + """Set up the test.""" + self.blockchain = Blockchain() + + def test_height(self) -> None: + """Test the 'height' property getter.""" + assert self.blockchain.height == 0 + + def test_len(self) -> None: + """Test the 'length' property getter.""" + assert self.blockchain.length == 0 + + def test_add_block_positive(self) -> None: + """Test 'add_block', success.""" + block = Block(MagicMock(height=1), []) + self.blockchain.add_block(block) + assert self.blockchain.length == 1 + assert self.blockchain.height == 1 + + def test_add_block_negative_wrong_height(self) -> None: + """Test 'add_block', wrong height.""" + wrong_height = 42 + block = Block(MagicMock(height=wrong_height), []) + with pytest.raises( + AddBlockError, + match=f"expected height {self.blockchain.height + 1}, got {wrong_height}", + ): + self.blockchain.add_block(block) + + def test_add_block_before_initial_height(self) -> None: + """Test 'add_block', too old height.""" + height_offset = 42 + blockchain = Blockchain(height_offset=height_offset) + block = Block(MagicMock(height=height_offset - 1), []) + blockchain.add_block(block) + + def test_blocks(self) -> None: + """Test 'blocks' property getter.""" + assert self.blockchain.blocks == tuple() + + +class TestBlockBuilder: + """Test block builder.""" + + def setup(self) -> None: + """Set up the method.""" + self.block_builder = BlockBuilder() + + def test_get_header_positive(self) -> None: + """Test header property getter, positive.""" + expected_header = MagicMock() + self.block_builder._current_header = expected_header + actual_header = self.block_builder.header + assert expected_header == actual_header + + def test_get_header_negative(self) -> None: + """Test header property getter, negative.""" + with pytest.raises(ValueError, match="header not set"): + self.block_builder.header + + def test_set_header_positive(self) -> None: + """Test header property setter, positive.""" + expected_header = MagicMock() + self.block_builder.header = expected_header + actual_header = self.block_builder.header + assert expected_header == actual_header + + def test_set_header_negative(self) -> None: + """Test header property getter, negative.""" + self.block_builder.header = MagicMock() + with pytest.raises(ValueError, match="header already set"): + self.block_builder.header = MagicMock() + + def test_transitions_getter(self) -> None: + """Test 'transitions' property getter.""" + assert self.block_builder.transactions == tuple() + + def test_add_transitions(self) -> None: + """Test 'add_transition'.""" + transaction = MagicMock() + self.block_builder.add_transaction(transaction) + assert self.block_builder.transactions == (transaction,) + + def test_get_block_negative_header_not_set_yet(self) -> None: + """Test 'get_block', negative case (header not set yet).""" + with pytest.raises(ValueError, match="header not set"): + self.block_builder.get_block() + + def test_get_block_positive(self) -> None: + """Test 'get_block', positive case.""" + self.block_builder.header = MagicMock() + self.block_builder.get_block() + + +class TestAbciAppDB: + """Test 'AbciAppDB' class.""" + + def setup(self) -> None: + """Set up the tests.""" + self.participants = ("a", "b") + self.db = AbciAppDB( + setup_data=dict(participants=[self.participants]), + ) + + @pytest.mark.parametrize( + "data, setup_data", + ( + ({"participants": ["a", "b"]}, {"participants": ["a", "b"]}), + ({"participants": []}, {}), + ({"participants": None}, None), + ("participants", None), + (1, None), + (object(), None), + (["participants"], None), + ({"participants": [], "other": [1, 2]}, {"other": [1, 2]}), + ), + ) + @pytest.mark.parametrize( + "cross_period_persisted_keys, expected_cross_period_persisted_keys", + ((None, set()), (set(), set()), ({"test"}, {"test"})), + ) + def test_init( + self, + data: Dict, + setup_data: Optional[Dict], + cross_period_persisted_keys: Optional[Set[str]], + expected_cross_period_persisted_keys: Set[str], + ) -> None: + """Test constructor.""" + # keys are a set, but we cast them to a frozenset, so we can still update them and also make `mypy` + # think that the type is correct, to simulate a user incorrectly passing a different type and check if the + # attribute can be altered + cast_keys = cast(Optional[FrozenSet[str]], cross_period_persisted_keys) + # update with the default keys + expected_cross_period_persisted_keys.update(AbciAppDB.default_cross_period_keys) + + if setup_data is None: + # the parametrization of `setup_data` set to `None` is in order to check if the exception is raised + # when we incorrectly set the data in the configuration file with a type that is not allowed + with pytest.raises( + ValueError, + match=re.escape( + f"AbciAppDB data must be `Dict[str, List[Any]]`, found `{type(data)}` instead" + ), + ): + AbciAppDB( + data, + ) + return + + # use copies because otherwise the arguments will be modified and the next test runs will be polluted + data_copy = deepcopy(data) + cross_period_persisted_keys_copy = cast_keys.copy() if cast_keys else cast_keys + db = AbciAppDB(data_copy, cross_period_persisted_keys_copy) + assert db._data == {0: setup_data} + assert db.setup_data == setup_data + assert db.cross_period_persisted_keys == expected_cross_period_persisted_keys + + def data_assertion() -> None: + """Assert that the data are fine.""" + assert db._data == {0: setup_data} and db.setup_data == setup_data, ( + "The database's `setup_data` have been altered indirectly, " + "by updating an item passed via the `__init__`!" + ) + + new_value_attempt = "new_value_attempt" + data_copy.update({"dummy_key": [new_value_attempt]}) + data_assertion() + + data_copy["participants"].append(new_value_attempt) + data_assertion() + + if cross_period_persisted_keys_copy: + # cast back to set + cast(Set[str], cross_period_persisted_keys_copy).add(new_value_attempt) + assert ( + db.cross_period_persisted_keys == expected_cross_period_persisted_keys + ), ( + "The database's `cross_period_persisted_keys` have been altered indirectly, " + "by updating an item passed via the `__init__`!" + ) + + class EnumTest(Enum): + """A test Enum class""" + + test = 10 + + @pytest.mark.parametrize( + "data_in, expected_output", + ( + (0, 0), + ([], []), + ({"test": 2}, {"test": 2}), + (EnumTest.test, 10), + (b"test", b"test".hex()), + ({3, 4}, "[3, 4]"), + (object(), None), + ), + ) + def test_normalize(self, data_in: Any, expected_output: Any) -> None: + """Test `normalize`.""" + if expected_output is None: + with pytest.raises(ValueError): + self.db.normalize(data_in) + return + assert self.db.normalize(data_in) == expected_output + + @pytest.mark.parametrize("data", {0: [{"test": 2}]}) + def test_reset_index(self, data: Dict) -> None: + """Test `reset_index`.""" + assert self.db.reset_index == 0 + self.db.sync(self.db.serialize()) + assert self.db.reset_index == 0 + + def test_round_count_setter(self) -> None: + """Tests the round count setter.""" + expected_value = 1 + + # assume the round count is 0 in the begging + self.db._round_count = 0 + + # update to one via the setter + self.db.round_count = expected_value + + assert self.db.round_count == expected_value + + def test_try_alter_init_data(self) -> None: + """Test trying to alter the init data.""" + data_key = "test" + data_value = [data_key] + expected_data = {data_key: data_value} + passed_data = {data_key: data_value.copy()} + db = AbciAppDB(passed_data) + assert db.setup_data == expected_data + + mutability_error_message = ( + "The database's `setup_data` have been altered indirectly, " + "by updating an item retrieved via the `setup_data` property!" + ) + + db.setup_data.update({data_key: ["altered"]}) + assert db.setup_data == expected_data, mutability_error_message + + db.setup_data[data_key].append("altered") + assert db.setup_data == expected_data, mutability_error_message + + def test_cross_period_persisted_keys(self) -> None: + """Test `cross_period_persisted_keys` property""" + setup_data: Dict[str, List] = {} + cross_period_persisted_keys = frozenset({"test"}) + db = AbciAppDB(setup_data, cross_period_persisted_keys.copy()) + + assert isinstance(db.cross_period_persisted_keys, frozenset), ( + "The database's `cross_period_persisted_keys` can be altered indirectly. " + "The `cross_period_persisted_keys` was expected to be a `frozenset`!" + ) + + def test_get(self) -> None: + """Test getters.""" + assert self.db.get("participants", default="default") == self.participants + assert self.db.get("inexistent", default="default") == "default" + assert self.db.get_latest_from_reset_index(0) == { + "participants": self.participants + } + assert self.db.get_latest() == {"participants": self.participants} + + mutable_key = "mutable" + mutable_value = ["test"] + self.db.update(**{mutable_key: mutable_value.copy()}) + mutable_getters = set() + for getter, kwargs in ( + ("get", {"key": mutable_key}), + ("get_strict", {"key": mutable_key}), + ("get_latest_from_reset_index", {"reset_index": 0}), + ("get_latest", {}), + ): + retrieved = getattr(self.db, getter)(**kwargs) + if getter.startswith("get_latest"): + retrieved = retrieved[mutable_key] + retrieved.append("new_value_attempt") + + if self.db.get(mutable_key) != mutable_value: + mutable_getters.add(getter) + + assert not mutable_getters, ( + "The database has been altered indirectly, " + f"by updating the item(s) retrieved via the `{mutable_getters}` method(s)!" + ) + + def test_increment_round_count(self) -> None: + """Test increment_round_count.""" + assert self.db.round_count == -1 + self.db.increment_round_count() + assert self.db.round_count == 0 + + @mock.patch.object( + abci_base, + "is_json_serializable", + return_value=False, + ) + def test_validate(self, _: mock._patch) -> None: + """Test `validate` method.""" + data = "does not matter" + + with pytest.raises( + ABCIAppInternalError, + match=re.escape( + "internal error: `AbciAppDB` data must be json-serializable. Please convert non-serializable data in " + f"`{data}`. You may use `AbciAppDB.validate(your_data)` to validate your data for the `AbciAppDB`." + ), + ): + AbciAppDB.validate(data) + + @pytest.mark.parametrize( + "setup_data, update_data, expected_data", + ( + (dict(), {"dummy_key": "dummy_value"}, {0: {"dummy_key": ["dummy_value"]}}), + ( + dict(), + {"dummy_key": ["dummy_value1", "dummy_value2"]}, + {0: {"dummy_key": [["dummy_value1", "dummy_value2"]]}}, + ), + ( + {"test": ["test"]}, + {"dummy_key": "dummy_value"}, + {0: {"dummy_key": ["dummy_value"], "test": ["test"]}}, + ), + ( + {"test": ["test"]}, + {"test": "dummy_value"}, + {0: {"test": ["test", "dummy_value"]}}, + ), + ( + {"test": [["test"]]}, + {"test": ["dummy_value1", "dummy_value2"]}, + {0: {"test": [["test"], ["dummy_value1", "dummy_value2"]]}}, + ), + ( + {"test": ["test"]}, + {"test": ["dummy_value1", "dummy_value2"]}, + {0: {"test": ["test", ["dummy_value1", "dummy_value2"]]}}, + ), + ), + ) + def test_update( + self, setup_data: Dict, update_data: Dict, expected_data: Dict[int, Dict] + ) -> None: + """Test update db.""" + db = AbciAppDB(setup_data) + db.update(**update_data) + assert db._data == expected_data + + mutable_key = "mutable" + mutable_value = ["test"] + update_data = {mutable_key: mutable_value.copy()} + db.update(**update_data) + + update_data[mutable_key].append("new_value_attempt") + assert ( + db.get(mutable_key) == mutable_value + ), "The database has been altered indirectly, by updating the item passed via the `update` method!" + + @pytest.mark.parametrize( + "replacement_value, expected_replacement", + ( + (132, 132), + ("test", "test"), + (set("132"), ("1", "2", "3")), + ({"132"}, ("132",)), + (frozenset("231"), ("1", "2", "3")), + (frozenset({"231"}), ("231",)), + (("1", "3", "2"), ("1", "3", "2")), + (["1", "5", "3"], ["1", "5", "3"]), + ), + ) + @pytest.mark.parametrize( + "setup_data, cross_period_persisted_keys", + ( + (dict(), frozenset()), + ({"test": [["test"]]}, frozenset()), + ({"test": [["test"]]}, frozenset({"test"})), + ({"test": ["test"]}, frozenset({"test"})), + ), + ) + def test_create( + self, + replacement_value: Any, + expected_replacement: Any, + setup_data: Dict, + cross_period_persisted_keys: FrozenSet[str], + ) -> None: + """Test `create` db.""" + db = AbciAppDB(setup_data) + db._cross_period_persisted_keys = cross_period_persisted_keys + db.create() + assert db._data == { + 0: setup_data, + 1: setup_data if cross_period_persisted_keys else {}, + }, "`create` did not produce the expected result!" + + mutable_key = "mutable" + mutable_value = ["test"] + existing_key = "test" + create_data = { + mutable_key: mutable_value.copy(), + existing_key: replacement_value, + } + db.create(**create_data) + + assert db._data == { + 0: setup_data, + 1: setup_data if cross_period_persisted_keys else {}, + 2: db.data_to_lists( + { + mutable_key: mutable_value.copy(), + existing_key: expected_replacement, + } + ), + }, "`create` did not produce the expected result!" + + create_data[mutable_key].append("new_value_attempt") + assert ( + db.get(mutable_key) == mutable_value + ), "The database has been altered indirectly, by updating the item passed via the `create` method!" + + def test_create_key_not_in_db(self) -> None: + """Test the `create` method when a given or a cross-period key does not exist in the db.""" + existing_key = "existing_key" + non_existing_key = "non_existing_key" + + db = AbciAppDB({existing_key: ["test_value"]}) + db._cross_period_persisted_keys = frozenset({non_existing_key}) + with pytest.raises( + ABCIAppInternalError, + match=f"Cross period persisted key `{non_existing_key}` " + "was not found in the db but was required for the next period.", + ): + db.create() + + db._cross_period_persisted_keys = frozenset({existing_key}) + db.create(**{non_existing_key: "test_value"}) + + @pytest.mark.parametrize( + "existing_data, cleanup_history_depth, cleanup_history_depth_current, expected", + ( + ( + {1: {"test": ["test", ["dummy_value1", "dummy_value2"]]}}, + 0, + None, + {1: {"test": ["test", ["dummy_value1", "dummy_value2"]]}}, + ), + ( + { + 1: {"test": ["test", ["dummy_value1", "dummy_value2"]]}, + 2: {"test": [0]}, + }, + 0, + None, + {2: {"test": [0]}}, + ), + ( + { + 1: {"test": ["test", ["dummy_value1", "dummy_value2"]]}, + 2: {"test": [0, 1, 2]}, + }, + 0, + 0, + {2: {"test": [0, 1, 2]}}, + ), + ( + { + 1: {"test": ["test", ["dummy_value1", "dummy_value2"]]}, + 2: {"test": [0, 1, 2]}, + }, + 0, + 1, + {2: {"test": [2]}}, + ), + ( + { + 1: {"test": ["test", ["dummy_value1", "dummy_value2"]]}, + 2: {"test": list(range(5))}, + 3: {"test": list(range(5, 10))}, + 4: {"test": list(range(10, 15))}, + 5: {"test": list(range(15, 20))}, + }, + 3, + 0, + { + 3: {"test": list(range(5, 10))}, + 4: {"test": list(range(10, 15))}, + 5: {"test": list(range(15, 20))}, + }, + ), + ( + { + 1: {"test": ["test", ["dummy_value1", "dummy_value2"]]}, + 2: {"test": list(range(5))}, + 3: {"test": list(range(5, 10))}, + 4: {"test": list(range(10, 15))}, + 5: {"test": list(range(15, 20))}, + }, + 5, + 3, + { + 1: {"test": ["test", ["dummy_value1", "dummy_value2"]]}, + 2: {"test": list(range(5))}, + 3: {"test": list(range(5, 10))}, + 4: {"test": list(range(10, 15))}, + 5: {"test": list(range(15 + 2, 20))}, + }, + ), + ( + { + 1: {"test": ["test", ["dummy_value1", "dummy_value2"]]}, + 2: {"test": list(range(5))}, + 3: {"test": list(range(5, 10))}, + 4: {"test": list(range(10, 15))}, + 5: {"test": list(range(15, 20))}, + }, + 2, + 3, + { + 4: {"test": list(range(10, 15))}, + 5: {"test": list(range(15 + 2, 20))}, + }, + ), + ( + { + 1: {"test": ["test", ["dummy_value1", "dummy_value2"]]}, + 2: {"test": list(range(5))}, + 3: {"test": list(range(5, 10))}, + 4: {"test": list(range(10, 15))}, + 5: {"test": list(range(15, 20))}, + }, + 0, + 1, + { + 5: {"test": [19]}, + }, + ), + ), + ) + def test_cleanup( + self, + existing_data: Dict[int, Dict[str, List[Any]]], + cleanup_history_depth: int, + cleanup_history_depth_current: Optional[int], + expected: Dict[int, Dict[str, List[Any]]], + ) -> None: + """Test cleanup db.""" + db = AbciAppDB({}) + db._cross_period_persisted_keys = frozenset() + for _, _data in existing_data.items(): + db._create_from_keys(**_data) + + db.cleanup(cleanup_history_depth, cleanup_history_depth_current) + assert db._data == expected + + def test_serialize(self) -> None: + """Test `serialize` method.""" + assert ( + self.db.serialize() + == '{"db_data": {"0": {"participants": [["a", "b"]]}}, "slashing_config": ""}' + ) + + @pytest.mark.parametrize( + "_data", + ({"db_data": {0: {"test": [0]}}, "slashing_config": "serialized_config"},), + ) + def test_sync(self, _data: Dict[str, Dict[int, Dict[str, List[Any]]]]) -> None: + """Test `sync` method.""" + try: + serialized_data = json.dumps(_data) + except TypeError as exc: + raise AssertionError( + "Incorrectly parametrized test. Data must be json serializable." + ) from exc + + self.db.sync(serialized_data) + assert self.db._data == _data["db_data"] + assert self.db.slashing_config == _data["slashing_config"] + + @pytest.mark.parametrize( + "serialized_data, match", + ( + (b"", "Could not decode data using "), + ( + json.dumps({"both_mandatory_keys_missing": {}}), + "internal error: Mandatory keys `db_data`, `slashing_config` are missing from the deserialized data: " + "{'both_mandatory_keys_missing': {}}\nThe following serialized data were given: " + '{"both_mandatory_keys_missing": {}}', + ), + ( + json.dumps({"db_data": {}}), + "internal error: Mandatory keys `db_data`, `slashing_config` are missing from the deserialized data: " + "{'db_data': {}}\nThe following serialized data were given: {\"db_data\": {}}", + ), + ( + json.dumps({"slashing_config": {}}), + "internal error: Mandatory keys `db_data`, `slashing_config` are missing from the deserialized data: " + "{'slashing_config': {}}\nThe following serialized data were given: {\"slashing_config\": {}}", + ), + ( + json.dumps( + {"db_data": {"invalid_index": {}}, "slashing_config": "anything"} + ), + "An invalid index was found while trying to sync the db using data: ", + ), + ( + json.dumps({"db_data": "invalid", "slashing_config": "anything"}), + "Could not decode db data with an invalid format: ", + ), + ), + ) + def test_sync_incorrect_data(self, serialized_data: Any, match: str) -> None: + """Test `sync` method with incorrect data.""" + with pytest.raises( + ABCIAppInternalError, + match=match, + ): + self.db.sync(serialized_data) + + def test_hash(self) -> None: + """Test `hash` method.""" + expected_hash = ( + b"\xd0^\xb0\x85\xf1\xf5\xd2\xe8\xe8\x85\xda\x1a\x99k" + b"\x1c\xde\xfa1\x8a\x87\xcc\xd7q?\xdf\xbbofz\xfb\x7fI" + ) + assert self.db.hash() == expected_hash + + +class TestBaseSynchronizedData: + """Test 'BaseSynchronizedData' class.""" + + def setup(self) -> None: + """Set up the tests.""" + self.participants = ("a", "b") + self.base_synchronized_data = BaseSynchronizedData( + db=AbciAppDB(setup_data=dict(participants=[self.participants])) + ) + + @given(text()) + def test_slashing_config(self, slashing_config: str) -> None: + """Test the `slashing_config` property.""" + self.base_synchronized_data.slashing_config = slashing_config + assert ( + self.base_synchronized_data.slashing_config + == self.base_synchronized_data.db.slashing_config + == slashing_config + ) + + def test_participants_getter_positive(self) -> None: + """Test 'participants' property getter.""" + assert frozenset(self.participants) == self.base_synchronized_data.participants + + def test_nb_participants_getter(self) -> None: + """Test 'participants' property getter.""" + assert len(self.participants) == self.base_synchronized_data.nb_participants + + def test_participants_getter_negative(self) -> None: + """Test 'participants' property getter, negative case.""" + base_synchronized_data = BaseSynchronizedData(db=AbciAppDB(setup_data={})) + # with pytest.raises(ValueError, match="Value of key=participants is None"): + with pytest.raises( + ValueError, + match=re.escape( + "'participants' field is not set for this period [0] and no default value was provided." + ), + ): + base_synchronized_data.participants + + def test_update(self) -> None: + """Test the 'update' method.""" + participants = ("a",) + expected = BaseSynchronizedData( + db=AbciAppDB(setup_data=dict(participants=[participants])) + ) + actual = self.base_synchronized_data.update(participants=participants) + assert expected.participants == actual.participants + assert actual.db._data == {0: {"participants": [("a", "b"), ("a",)]}} + + def test_create(self) -> None: + """Test the 'create' method.""" + self.base_synchronized_data.db._cross_period_persisted_keys = frozenset( + {"participants"} + ) + assert self.base_synchronized_data.db._data == { + 0: {"participants": [("a", "b")]} + } + actual = self.base_synchronized_data.create() + assert actual.db._data == { + 0: {"participants": [("a", "b")]}, + 1: {"participants": [("a", "b")]}, + } + + def test_repr(self) -> None: + """Test the '__repr__' magic method.""" + actual_repr = repr(self.base_synchronized_data) + expected_repr_regex = r"BaseSynchronizedData\(db=AbciAppDB\({(.*)}\)\)" + assert re.match(expected_repr_regex, actual_repr) is not None + + def test_participants_list_is_empty( + self, + ) -> None: + """Tets when participants list is set to zero.""" + base_synchronized_data = BaseSynchronizedData( + db=AbciAppDB(setup_data=dict(participants=[tuple()])) + ) + with pytest.raises(ValueError, match="List participants cannot be empty."): + _ = base_synchronized_data.participants + + def test_all_participants_list_is_empty( + self, + ) -> None: + """Tets when participants list is set to zero.""" + base_synchronized_data = BaseSynchronizedData( + db=AbciAppDB(setup_data=dict(all_participants=[tuple()])) + ) + with pytest.raises(ValueError, match="List participants cannot be empty."): + _ = base_synchronized_data.all_participants + + @pytest.mark.parametrize( + "n_participants, given_threshold, expected_threshold", + ( + (1, None, 1), + (5, None, 4), + (10, None, 7), + (345, None, 231), + (246236, None, 164158), + (1, 1, 1), + (5, 5, 5), + (10, 7, 7), + (10, 8, 8), + (10, 9, 9), + (10, 10, 10), + (345, 300, 300), + (246236, 194158, 194158), + ), + ) + def test_consensus_threshold( + self, n_participants: int, given_threshold: int, expected_threshold: int + ) -> None: + """Test the `consensus_threshold` property.""" + base_synchronized_data = BaseSynchronizedData( + db=AbciAppDB( + setup_data=dict( + all_participants=[tuple(range(n_participants))], + consensus_threshold=[given_threshold], + ) + ) + ) + + assert base_synchronized_data.consensus_threshold == expected_threshold + + @pytest.mark.parametrize( + "n_participants, given_threshold", + ( + (1, 2), + (5, 2), + (10, 4), + (10, 11), + (10, 18), + (345, 200), + (246236, 164157), + (246236, 246237), + ), + ) + def test_consensus_threshold_incorrect( + self, + n_participants: int, + given_threshold: int, + ) -> None: + """Test the `consensus_threshold` property when an incorrect threshold value has been inserted to the db.""" + base_synchronized_data = BaseSynchronizedData( + db=AbciAppDB( + setup_data=dict( + all_participants=[tuple(range(n_participants))], + consensus_threshold=[given_threshold], + ) + ) + ) + + with pytest.raises(ValueError, match="Consensus threshold "): + _ = base_synchronized_data.consensus_threshold + + def test_properties(self) -> None: + """Test several properties""" + participants = ["b", "a"] + randomness_str = ( + "3439d92d58e47d342131d446a3abe264396dd264717897af30525c98408c834f" + ) + randomness_value = 0.20400769574270503 + most_voted_keeper_address = "most_voted_keeper_address" + blacklisted_keepers = "blacklisted_keepers" + participant_to_selection = participant_to_randomness = participant_to_votes = { + "sender": DummyPayload(sender="sender", dummy_attribute=0) + } + safe_contract_address = "0x0" + + base_synchronized_data = BaseSynchronizedData( + db=AbciAppDB( + setup_data=AbciAppDB.data_to_lists( + dict( + participants=participants, + all_participants=participants, + most_voted_randomness=randomness_str, + most_voted_keeper_address=most_voted_keeper_address, + blacklisted_keepers=blacklisted_keepers, + participant_to_selection=CollectionRound.serialize_collection( + participant_to_selection + ), + participant_to_randomness=CollectionRound.serialize_collection( + participant_to_randomness + ), + participant_to_votes=CollectionRound.serialize_collection( + participant_to_votes + ), + safe_contract_address=safe_contract_address, + ) + ) + ) + ) + assert self.base_synchronized_data.period_count == 0 + assert base_synchronized_data.all_participants == frozenset(participants) + assert base_synchronized_data.sorted_participants == ["a", "b"] + assert base_synchronized_data.max_participants == len(participants) + assert abs(base_synchronized_data.keeper_randomness - randomness_value) < 1e-10 + assert base_synchronized_data.most_voted_randomness == randomness_str + assert ( + base_synchronized_data.most_voted_keeper_address + == most_voted_keeper_address + ) + assert base_synchronized_data.is_keeper_set + assert base_synchronized_data.blacklisted_keepers == {blacklisted_keepers} + assert ( + base_synchronized_data.participant_to_selection == participant_to_selection + ) + assert ( + base_synchronized_data.participant_to_randomness + == participant_to_randomness + ) + assert base_synchronized_data.participant_to_votes == participant_to_votes + assert base_synchronized_data.safe_contract_address == safe_contract_address + + +class DummyConcreteRound(AbstractRound): + """A dummy concrete round's implementation.""" + + payload_class: Optional[Type[BaseTxPayload]] = None + synchronized_data_class = MagicMock() + payload_attribute = MagicMock() + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, EventType]]: + """A dummy `end_block` implementation.""" + + def check_payload(self, payload: BaseTxPayload) -> None: + """A dummy `check_payload` implementation.""" + + def process_payload(self, payload: BaseTxPayload) -> None: + """A dummy `process_payload` implementation.""" + + +class TestAbstractRound: + """Test the 'AbstractRound' class.""" + + def setup(self) -> None: + """Set up the tests.""" + self.known_payload_type = ConcreteRoundA.payload_class + self.participants = ("a", "b") + self.base_synchronized_data = BaseSynchronizedData( + db=AbciAppDB( + setup_data=dict( + all_participants=[self.participants], + participants=[self.participants], + consensus_threshold=[2], + ) + ) + ) + self.round = ConcreteRoundA(self.base_synchronized_data, MagicMock()) + + def test_auto_round_id(self) -> None: + """Test that the 'auto_round_id()' method works as expected.""" + + assert DummyConcreteRound.auto_round_id() == "dummy_concrete_round" + + def test_must_not_set_round_id(self) -> None: + """Test that the 'round_id' must be set in concrete classes.""" + + # no exception as round id is auto-assigned + my_concrete_round = DummyConcreteRound(MagicMock(), MagicMock()) + assert my_concrete_round.round_id == "dummy_concrete_round" + + def test_must_set_payload_class_type(self) -> None: + """Test that the 'payload_class' must be set in concrete classes.""" + + with pytest.raises( + AbstractRoundInternalError, match="'payload_class' not set on .*" + ): + + class MyConcreteRound(AbstractRound): + synchronized_data_class = MagicMock() + payload_attribute = MagicMock() + # here payload_class is missing + + def test_check_payload_type_with_previous_round_transaction(self) -> None: + """Test check 'check_payload_type'.""" + + class MyConcreteRound(DummyConcreteRound): + """A concrete round with the payload class defined.""" + + payload_class = BaseTxPayload + + with pytest.raises(LateArrivingTransaction): + MyConcreteRound(MagicMock(), MagicMock(), BaseTxPayload).check_payload_type( + MagicMock(payload=BaseTxPayload("dummy")) + ) + + def test_check_payload_type(self) -> None: + """Test check 'check_payload_type'.""" + + with pytest.raises( + TransactionTypeNotRecognizedError, + match="current round does not allow transactions", + ): + DummyConcreteRound(MagicMock(), MagicMock()).check_payload_type(MagicMock()) + + def test_synchronized_data_getter(self) -> None: + """Test 'synchronized_data' property getter.""" + state = self.round.synchronized_data + assert state.participants == frozenset(self.participants) + + def test_check_transaction_unknown_payload(self) -> None: + """Test 'check_transaction' method, with unknown payload type.""" + tx_type = "unknown_payload" + tx_mock = MagicMock() + tx_mock.payload_class = tx_type + with pytest.raises( + TransactionTypeNotRecognizedError, + match="request '.*' not recognized", + ): + self.round.check_transaction(tx_mock) + + def test_check_transaction_known_payload(self) -> None: + """Test 'check_transaction' method, with known payload type.""" + tx_mock = MagicMock() + tx_mock.payload = self.known_payload_type(sender="dummy") + self.round.check_transaction(tx_mock) + + def test_process_transaction_negative_unknown_payload(self) -> None: + """Test 'process_transaction' method, with unknown payload type.""" + tx_mock = MagicMock() + tx_mock.payload = object + with pytest.raises( + TransactionTypeNotRecognizedError, + match="request '.*' not recognized", + ): + self.round.process_transaction(tx_mock) + + def test_process_transaction_negative_check_transaction_fails(self) -> None: + """Test 'process_transaction' method, with 'check_transaction' failing.""" + tx_mock = MagicMock() + tx_mock.payload = object + error_message = "transaction not valid" + with mock.patch.object( + self.round, "check_payload_type", side_effect=ValueError(error_message) + ): + with pytest.raises(ValueError, match=error_message): + self.round.process_transaction(tx_mock) + + def test_process_transaction_positive(self) -> None: + """Test 'process_transaction' method, positive case.""" + tx_mock = MagicMock() + tx_mock.payload = BaseTxPayload(sender="dummy") + self.round.process_transaction(tx_mock) + + def test_check_majority_possible_raises_error_when_nb_participants_is_0( + self, + ) -> None: + """Check that 'check_majority_possible' raises error when nb_participants=0.""" + with pytest.raises( + ABCIAppInternalError, + match="nb_participants not consistent with votes_by_participants", + ): + DummyConcreteRound( + self.base_synchronized_data, MagicMock() + ).check_majority_possible({}, 0) + + def test_check_majority_possible_passes_when_vote_set_is_empty(self) -> None: + """Check that 'check_majority_possible' passes when the set of votes is empty.""" + DummyConcreteRound( + self.base_synchronized_data, MagicMock() + ).check_majority_possible({}, 1) + + def test_check_majority_possible_passes_when_vote_set_nonempty_and_check_passes( + self, + ) -> None: + """ + Check that 'check_majority_possible' passes when set of votes is non-empty. + + The check passes because: + - the threshold is 2 + - the other voter can vote for the same item of the first voter + """ + DummyConcreteRound( + self.base_synchronized_data, MagicMock() + ).check_majority_possible({"alice": DummyPayload("alice", True)}, 2) + + def test_check_majority_possible_passes_when_payload_attributes_majority_match( + self, + ) -> None: + """ + Test 'check_majority_possible' when set of votes is non-empty and the majority of the attribute values match. + + The check passes because: + - the threshold is 3 (participants are 4) + - 3 voters have the same attribute value in their payload + """ + DummyConcreteRound( + self.base_synchronized_data, MagicMock() + ).check_majority_possible( + { + "voter_1": DummyPayload("voter_1", 0), + "voter_2": DummyPayload("voter_2", 0), + "voter_3": DummyPayload("voter_3", 0), + }, + 4, + ) + + def test_check_majority_possible_passes_when_vote_set_nonempty_and_check_doesnt_pass( + self, + ) -> None: + """ + Check that 'check_majority_possible' doesn't pass when set of votes is non-empty. + + the check does not pass because: + - the threshold is 2 + - both voters have already voted for different items + """ + with pytest.raises( + ABCIAppException, + match="cannot reach quorum=2, number of remaining votes=0, number of most voted item's votes=1", + ): + DummyConcreteRound( + self.base_synchronized_data, MagicMock() + ).check_majority_possible( + { + "alice": DummyPayload("alice", False), + "bob": DummyPayload("bob", True), + }, + 2, + ) + + def test_is_majority_possible_positive_case(self) -> None: + """Test 'is_majority_possible', positive case.""" + assert DummyConcreteRound( + self.base_synchronized_data, MagicMock() + ).is_majority_possible({"alice": DummyPayload("alice", False)}, 2) + + def test_is_majority_possible_negative_case(self) -> None: + """Test 'is_majority_possible', negative case.""" + assert not DummyConcreteRound( + self.base_synchronized_data, MagicMock() + ).is_majority_possible( + { + "alice": DummyPayload("alice", False), + "bob": DummyPayload("bob", True), + }, + 2, + ) + + def test_check_majority_possible_raises_error_when_new_voter_already_voted( + self, + ) -> None: + """Test 'check_majority_possible_with_new_vote' raises when new voter already voted.""" + with pytest.raises(ABCIAppInternalError, match="voter has already voted"): + DummyConcreteRound( + self.base_synchronized_data, + MagicMock(), + ).check_majority_possible_with_new_voter( + {"alice": DummyPayload("alice", False)}, + "alice", + DummyPayload("alice", True), + 2, + ) + + def test_check_majority_possible_raises_error_when_nb_participants_inconsistent( + self, + ) -> None: + """Test 'check_majority_possible_with_new_vote' raises when 'nb_participants' inconsistent with other args.""" + with pytest.raises( + ABCIAppInternalError, + match="nb_participants not consistent with votes_by_participants", + ): + DummyConcreteRound( + self.base_synchronized_data, + MagicMock(), + ).check_majority_possible_with_new_voter( + {"alice": DummyPayload("alice", True)}, + "bob", + DummyPayload("bob", True), + 1, + ) + + def test_check_majority_possible_when_check_passes( + self, + ) -> None: + """ + Test 'check_majority_possible_with_new_vote' when the check passes. + + The test passes because: + - the number of participants is 2, and so the threshold is 2 + - the new voter votes for the same item already voted by voter 1. + """ + DummyConcreteRound( + self.base_synchronized_data, + MagicMock(), + ).check_majority_possible_with_new_voter( + {"alice": DummyPayload("alice", True)}, "bob", DummyPayload("bob", True), 2 + ) + + +class TestTimeouts: + """Test the 'Timeouts' class.""" + + def setup(self) -> None: + """Set up the test.""" + self.timeouts: Timeouts = Timeouts() + + def test_size(self) -> None: + """Test the 'size' property.""" + assert self.timeouts.size == 0 + self.timeouts._heap.append(MagicMock()) + assert self.timeouts.size == 1 + + def test_add_timeout(self) -> None: + """Test the 'add_timeout' method.""" + # the first time, entry_count = 0 + entry_count = self.timeouts.add_timeout(datetime.datetime.now(), MagicMock()) + assert entry_count == 0 + + # the second time, entry_count is incremented + entry_count = self.timeouts.add_timeout(datetime.datetime.now(), MagicMock()) + assert entry_count == 1 + + def test_cancel_timeout(self) -> None: + """Test the 'cancel_timeout' method.""" + entry_count = self.timeouts.add_timeout(datetime.datetime.now(), MagicMock()) + assert self.timeouts.size == 1 + + self.timeouts.cancel_timeout(entry_count) + + # cancelling timeouts does not remove them from the heap + assert self.timeouts.size == 1 + + def test_pop_earliest_cancelled_timeouts(self) -> None: + """Test the 'pop_earliest_cancelled_timeouts' method.""" + entry_count_1 = self.timeouts.add_timeout(datetime.datetime.now(), MagicMock()) + entry_count_2 = self.timeouts.add_timeout(datetime.datetime.now(), MagicMock()) + self.timeouts.cancel_timeout(entry_count_1) + self.timeouts.cancel_timeout(entry_count_2) + self.timeouts.pop_earliest_cancelled_timeouts() + assert self.timeouts.size == 0 + + def test_get_earliest_timeout_a(self) -> None: + """Test the 'get_earliest_timeout' method.""" + deadline_1 = datetime.datetime.now() + event_1 = MagicMock() + + sleep(0.5) + + deadline_2 = datetime.datetime.now() + event_2 = MagicMock() + assert deadline_1 < deadline_2 + + self.timeouts.add_timeout(deadline_2, event_2) + self.timeouts.add_timeout(deadline_1, event_1) + + assert self.timeouts.size == 2 + # test that we get the event with the earliest deadline + timeout, event = self.timeouts.get_earliest_timeout() + assert timeout == deadline_1 + assert event == event_1 + + # test that get_earliest_timeout does not remove elements + assert self.timeouts.size == 2 + + popped_timeout, popped_event = self.timeouts.pop_timeout() + assert popped_timeout == timeout + assert popped_event == event + + def test_get_earliest_timeout_b(self) -> None: + """Test the 'get_earliest_timeout' method.""" + + deadline_1 = datetime.datetime.now() + event_1 = MagicMock() + + sleep(0.5) + + deadline_2 = datetime.datetime.now() + event_2 = MagicMock() + assert deadline_1 < deadline_2 + + self.timeouts.add_timeout(deadline_1, event_1) + self.timeouts.add_timeout(deadline_2, event_2) + + assert self.timeouts.size == 2 + # test that we get the event with the earliest deadline + timeout, event = self.timeouts.get_earliest_timeout() + assert timeout == deadline_1 + assert event == event_1 + + # test that get_earliest_timeout does not remove elements + assert self.timeouts.size == 2 + + def test_pop_timeout(self) -> None: + """Test the 'pop_timeout' method.""" + deadline_1 = datetime.datetime.now() + event_1 = MagicMock() + + sleep(0.5) + + deadline_2 = datetime.datetime.now() + event_2 = MagicMock() + assert deadline_1 < deadline_2 + + self.timeouts.add_timeout(deadline_2, event_2) + self.timeouts.add_timeout(deadline_1, event_1) + + assert self.timeouts.size == 2 + # test that we get the event with the earliest deadline + timeout, event = self.timeouts.pop_timeout() + assert timeout == deadline_1 + assert event == event_1 + + # test that pop_timeout removes elements + assert self.timeouts.size == 1 + + +STUB_TERMINATION_CONFIG = abci_base.BackgroundAppConfig( + round_cls=ConcreteBackgroundRound, + start_event=ConcreteEvents.TERMINATE, + abci_app=TerminationAppTest, +) + +STUB_SLASH_CONFIG = abci_base.BackgroundAppConfig( + round_cls=ConcreteBackgroundSlashingRound, + start_event=ConcreteEvents.SLASH_START, + end_event=ConcreteEvents.SLASH_END, + abci_app=SlashingAppTest, +) + + +class TestAbciApp: + """Test the 'AbciApp' class.""" + + def setup(self) -> None: + """Set up the test.""" + self.abci_app = AbciAppTest(MagicMock(), MagicMock(), MagicMock()) + self.abci_app.add_background_app(STUB_TERMINATION_CONFIG) + + def teardown(self) -> None: + """Teardown the test.""" + self.abci_app.background_apps.clear() + + @pytest.mark.parametrize("flag", (True, False)) + def test_is_abstract(self, flag: bool) -> None: + """Test `is_abstract` property.""" + + class CopyOfAbciApp(AbciAppTest): + """Copy to avoid side effects due to state change""" + + CopyOfAbciApp._is_abstract = flag + assert CopyOfAbciApp.is_abstract() is flag + + def test_initial_round_cls_not_set(self) -> None: + """Test when 'initial_round_cls' is not set.""" + + with pytest.raises( + ABCIAppInternalError, match="'initial_round_cls' field not set" + ): + + class MyAbciApp(AbciApp): + # here 'initial_round_cls' should be defined. + # ... + transition_function: AbciAppTransitionFunction = {} + + def test_transition_function_not_set(self) -> None: + """Test when 'transition_function' is not set.""" + with pytest.raises( + ABCIAppInternalError, match="'transition_function' field not set" + ): + + class MyAbciApp(AbciApp): + initial_round_cls = ConcreteRoundA + # here 'transition_function' should be defined. + # ... + + def test_last_timestamp_negative(self) -> None: + """Test the 'last_timestamp' property, negative case.""" + with pytest.raises(ABCIAppInternalError, match="last timestamp is None"): + self.abci_app.last_timestamp + + def test_last_timestamp_positive(self) -> None: + """Test the 'last_timestamp' property, positive case.""" + expected = MagicMock() + self.abci_app._last_timestamp = expected + assert expected == self.abci_app.last_timestamp + + @pytest.mark.parametrize( + "db_key, sync_classes, default, property_found", + ( + ("", set(), "default", False), + ("non_existing_key", {BaseSynchronizedData}, True, False), + ("participants", {BaseSynchronizedData}, {}, False), + ("is_keeper_set", {BaseSynchronizedData}, True, True), + ), + ) + def test_get_synced_value( + self, + db_key: str, + sync_classes: Set[Type[BaseSynchronizedData]], + default: Any, + property_found: bool, + ) -> None: + """Test the `_get_synced_value` method.""" + res = self.abci_app._get_synced_value(db_key, sync_classes, default) + if property_found: + assert res == getattr(self.abci_app.synchronized_data, db_key) + return + assert res == self.abci_app.synchronized_data.db.get(db_key, default) + + def test_process_event(self) -> None: + """Test the 'process_event' method, positive case, with timeout events.""" + self.abci_app.add_background_app(STUB_SLASH_CONFIG) + self.abci_app.setup() + self.abci_app._last_timestamp = MagicMock() + assert self.abci_app._transition_backup.transition_function is None + assert isinstance(self.abci_app.current_round, ConcreteRoundA) + self.abci_app.process_event(ConcreteEvents.B) + assert isinstance(self.abci_app.current_round, ConcreteRoundB) + self.abci_app.process_event(ConcreteEvents.TIMEOUT) + assert isinstance(self.abci_app.current_round, ConcreteRoundA) + self.abci_app.process_event(ConcreteEvents.TERMINATE) + assert isinstance(self.abci_app.current_round, ConcreteTerminationRoundA) + expected_backup = deepcopy(self.abci_app.transition_function) + assert ( + self.abci_app._transition_backup.transition_function + == AbciAppTest.transition_function + ) + self.abci_app.process_event(ConcreteEvents.SLASH_START) + assert isinstance(self.abci_app.current_round, ConcreteSlashingRoundA) + assert ( + self.abci_app._transition_backup.transition_function + == expected_backup + == TerminationAppTest.transition_function + ) + assert self.abci_app.transition_function == SlashingAppTest.transition_function + self.abci_app.process_event(ConcreteEvents.SLASH_END) + # should return back to the round that was running before the slashing started + assert isinstance(self.abci_app.current_round, ConcreteTerminationRoundA) + assert self.abci_app.transition_function == expected_backup + assert self.abci_app._transition_backup.transition_function is None + assert self.abci_app._transition_backup.round is None + + def test_process_event_negative_case(self) -> None: + """Test the 'process_event' method, negative case.""" + with mock.patch.object(self.abci_app.logger, "warning") as mock_warning: + self.abci_app.process_event(ConcreteEvents.A) + mock_warning.assert_called_with( + "Cannot process event 'a' as current state is not set" + ) + + def test_update_time(self) -> None: + """Test the 'update_time' method.""" + # schedule round_a + current_time = datetime.datetime.now() + self.abci_app.setup() + self.abci_app._last_timestamp = current_time + + # move to round_b that schedules timeout events + self.abci_app.process_event(ConcreteEvents.B) + assert self.abci_app.current_round_id == "concrete_round_b" + + # simulate most recent timestamp beyond earliest deadline + # after pop, len(timeouts) == 0, because round_a does not schedule new timeout events + current_time = current_time + datetime.timedelta(0, AbciAppTest.TIMEOUT) + self.abci_app.update_time(current_time) + + # now we are back to round_a + assert self.abci_app.current_round_id == "concrete_round_a" + + # move to round_c that schedules timeout events to itself + self.abci_app.process_event(ConcreteEvents.C) + assert self.abci_app.current_round_id == "concrete_round_c" + + # simulate most recent timestamp beyond earliest deadline + # after pop, len(timeouts) == 0, because round_c schedules timeout events + current_time = current_time + datetime.timedelta(0, AbciAppTest.TIMEOUT) + self.abci_app.update_time(current_time) + + assert self.abci_app.current_round_id == "concrete_round_c" + + # further update changes nothing + height = self.abci_app.current_round_height + self.abci_app.update_time(current_time) + assert height == self.abci_app.current_round_height + + def test_get_all_events(self) -> None: + """Test the all events getter.""" + assert { + ConcreteEvents.A, + ConcreteEvents.B, + ConcreteEvents.C, + ConcreteEvents.TIMEOUT, + } == self.abci_app.get_all_events() + + @pytest.mark.parametrize("include_background_rounds", (True, False)) + def test_get_all_rounds_classes( + self, + include_background_rounds: bool, + ) -> None: + """Test the get all rounds getter.""" + expected_rounds = {ConcreteRoundA, ConcreteRoundB, ConcreteRoundC} + + if include_background_rounds: + expected_rounds.update( + { + ConcreteBackgroundRound, + ConcreteTerminationRoundA, + ConcreteTerminationRoundB, + ConcreteTerminationRoundC, + } + ) + + actual_rounds = self.abci_app.get_all_round_classes( + {ConcreteBackgroundRound}, include_background_rounds + ) + + assert actual_rounds == expected_rounds + + def test_get_all_rounds_classes_bg_ever_running( + self, + ) -> None: + """Test the get all rounds when the background round is of an ever running type.""" + # we clear the pre-existing bg apps and add an ever running + self.abci_app.background_apps.clear() + self.abci_app.add_background_app( + abci_base.BackgroundAppConfig(ConcreteBackgroundRound) + ) + include_background_rounds = True + expected_rounds = { + ConcreteRoundA, + ConcreteRoundB, + ConcreteRoundC, + } + assert expected_rounds == self.abci_app.get_all_round_classes( + {ConcreteBackgroundRound}, include_background_rounds + ) + + def test_add_background_app(self) -> None: + """Tests the add method for the background apps.""" + # remove the terminating bg round added in `setup()` and the pending offences bg app added in the metaclass + self.abci_app.background_apps.clear() + + class EmptyAbciApp(AbciAppTest): + """An AbciApp without background apps' attributes set.""" + + cross_period_persisted_keys = frozenset({"1", "2"}) + + class BackgroundAbciApp(AbciAppTest): + """A mock background AbciApp.""" + + cross_period_persisted_keys = frozenset({"2", "3"}) + + assert len(EmptyAbciApp.background_apps) == 0 + assert EmptyAbciApp.cross_period_persisted_keys == {"1", "2"} + # add the background app + bg_app_config = abci_base.BackgroundAppConfig( + round_cls=ConcreteBackgroundRound, + start_event=ConcreteEvents.TERMINATE, + abci_app=BackgroundAbciApp, + ) + EmptyAbciApp.add_background_app(bg_app_config) + assert len(EmptyAbciApp.background_apps) == 1 + assert EmptyAbciApp.cross_period_persisted_keys == {"1", "2", "3"} + + def test_cleanup(self) -> None: + """Test the cleanup method.""" + self.abci_app.setup() + + # Dummy parameters, synchronized data and round + cleanup_history_depth = 1 + start_history_depth = 5 + max_participants = 4 + dummy_synchronized_data = BaseSynchronizedData( + db=AbciAppDB(setup_data=dict(participants=[max_participants])) + ) + dummy_round = ConcreteRoundA(dummy_synchronized_data, MagicMock()) + + # Add dummy data + self.abci_app._previous_rounds = [dummy_round] * start_history_depth + self.abci_app._round_results = [dummy_synchronized_data] * start_history_depth + self.abci_app.synchronized_data.db._data = { + i: {"dummy_key": ["dummy_value"]} for i in range(start_history_depth) + } + + round_height = self.abci_app.current_round_height + # Verify that cleanup reduces the data amount + assert len(self.abci_app._previous_rounds) == start_history_depth + assert len(self.abci_app._round_results) == start_history_depth + assert len(self.abci_app.synchronized_data.db._data) == start_history_depth + assert list(self.abci_app.synchronized_data.db._data.keys()) == list( + range(start_history_depth) + ) + previous_reset_index = self.abci_app.synchronized_data.db.reset_index + + self.abci_app.cleanup(cleanup_history_depth) + + assert len(self.abci_app._previous_rounds) == cleanup_history_depth + assert len(self.abci_app._round_results) == cleanup_history_depth + assert len(self.abci_app.synchronized_data.db._data) == cleanup_history_depth + assert list(self.abci_app.synchronized_data.db._data.keys()) == list( + range(start_history_depth - cleanup_history_depth, start_history_depth) + ) + # reset_index must not change after a cleanup + assert self.abci_app.synchronized_data.db.reset_index == previous_reset_index + + # Verify round height stays unaffected + assert self.abci_app.current_round_height == round_height + + # Add more values to the history + reset_index = self.abci_app.synchronized_data.db.reset_index + cleanup_history_depth_current = 3 + for _ in range(10): + self.abci_app.synchronized_data.db.update(dummy_key="dummy_value") + + # Check that the history cleanup keeps the desired history length + self.abci_app.cleanup_current_histories(cleanup_history_depth_current) + history_len = len( + self.abci_app.synchronized_data.db._data[reset_index]["dummy_key"] + ) + assert history_len == cleanup_history_depth_current + + @mock.patch.object(ConcreteBackgroundRound, "check_transaction") + @pytest.mark.parametrize( + "transaction", + [mock.MagicMock(payload=DUMMY_CONCRETE_BACKGROUND_PAYLOAD)], + ) + def test_check_transaction_for_termination_round( + self, + check_transaction_mock: mock.Mock, + transaction: Transaction, + ) -> None: + """Tests process_transaction when it's a transaction meant for the termination app.""" + self.abci_app.setup() + self.abci_app.check_transaction(transaction) + check_transaction_mock.assert_called_with(transaction) + + @mock.patch.object(ConcreteBackgroundRound, "process_transaction") + @pytest.mark.parametrize( + "transaction", + [mock.MagicMock(payload=DUMMY_CONCRETE_BACKGROUND_PAYLOAD)], + ) + def test_process_transaction_for_termination_round( + self, + process_transaction_mock: mock.Mock, + transaction: Transaction, + ) -> None: + """Tests process_transaction when it's a transaction meant for the termination app.""" + self.abci_app.setup() + self.abci_app.process_transaction(transaction) + process_transaction_mock.assert_called_with(transaction) + + +class TestOffenceTypeFns: + """Test `OffenceType`-related functions.""" + + @staticmethod + def test_light_offences() -> None: + """Test `light_offences` function.""" + assert list(light_offences()) == [ + OffenseType.VALIDATOR_DOWNTIME, + OffenseType.INVALID_PAYLOAD, + OffenseType.BLACKLISTED, + OffenseType.SUSPECTED, + ] + + @staticmethod + def test_serious_offences() -> None: + """Test `serious_offences` function.""" + assert list(serious_offences()) == [ + OffenseType.UNKNOWN, + OffenseType.DOUBLE_SIGNING, + OffenseType.LIGHT_CLIENT_ATTACK, + ] + + +@composite +def availability_window_data(draw: DrawFn) -> Dict[str, int]: + """A strategy for building valid availability window data.""" + max_length = draw(integers(min_value=1, max_value=12_000)) + array = draw(integers(min_value=0, max_value=(2**max_length) - 1)) + num_positive = draw(integers(min_value=0, max_value=1_000_000)) + num_negative = draw(integers(min_value=0, max_value=1_000_000)) + + return { + "max_length": max_length, + "array": array, + "num_positive": num_positive, + "num_negative": num_negative, + } + + +class TestAvailabilityWindow: + """Test `AvailabilityWindow`.""" + + @staticmethod + @given(integers(min_value=1, max_value=100)) + def test_not_equal(max_length: int) -> None: + """Test the `add` method.""" + availability_window_1 = AvailabilityWindow(max_length) + availability_window_2 = AvailabilityWindow(max_length) + assert availability_window_1 == availability_window_2 + availability_window_2.add(False) + assert availability_window_1 != availability_window_2 + # test with a different type + assert availability_window_1 != MagicMock() + + @staticmethod + @given(integers(min_value=0, max_value=100), data()) + def test_add(max_length: int, hypothesis_data: Any) -> None: + """Test the `add` method.""" + if max_length < 1: + with pytest.raises( + ValueError, + match=f"An `AvailabilityWindow` with a `max_length` {max_length} < 1 is not valid.", + ): + AvailabilityWindow(max_length) + return + + availability_window = AvailabilityWindow(max_length) + + expected_positives = expected_negatives = 0 + for i in range(max_length): + value = hypothesis_data.draw(booleans()) + availability_window.add(value) + items_in = i + 1 + assert len(availability_window._window) == items_in + assert availability_window._window[-1] is value + expected_positives += 1 if value else 0 + assert availability_window._num_positive == expected_positives + expected_negatives = items_in - expected_positives + assert availability_window._num_negative == expected_negatives + + # max length is reached and window starts cycling + assert len(availability_window._window) == max_length + for _ in range(10): + value = hypothesis_data.draw(booleans()) + expected_popped_value = ( + None if max_length == 0 else availability_window._window[0] + ) + availability_window.add(value) + assert len(availability_window._window) == max_length + if expected_popped_value is not None: + expected_positives -= bool(expected_popped_value) + expected_negatives -= bool(not expected_popped_value) + expected_positives += bool(value) + expected_negatives += bool(not value) + assert availability_window._num_positive == expected_positives + assert availability_window._num_negative == expected_negatives + + @staticmethod + @given( + max_length=integers(min_value=1, max_value=30_000), + num_positive=integers(min_value=0), + num_negative=integers(min_value=0), + ) + @pytest.mark.parametrize( + "window, expected_serialization", + ( + (deque(()), 0), + (deque((False, False, False)), 0), + (deque((True, False, True)), 5), + (deque((True for _ in range(3))), 7), + ( + deque((True for _ in range(1000))), + int( + "10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958" + "58127594672917553146825187145285692314043598457757469857480393456777482423098542107460506237114187" + "79541821530464749835819412673987675591655439460770629145711964776865421676604298316526243868372056" + "68069375" + ), + ), + ), + ) + def test_to_dict( + max_length: int, + num_positive: int, + num_negative: int, + window: Deque, + expected_serialization: int, + ) -> None: + """Test `to_dict` method.""" + availability_window = AvailabilityWindow(max_length) + availability_window._num_positive = num_positive + availability_window._num_negative = num_negative + availability_window._window = window + assert availability_window.to_dict() == { + "max_length": max_length, + "array": expected_serialization, + "num_positive": num_positive, + "num_negative": num_negative, + } + + @staticmethod + @pytest.mark.parametrize( + "data_, key, validator, expected_error", + ( + ({"a": 1, "b": 2, "c": 3}, "a", lambda x: x > 0, None), + ( + {"a": 1, "b": 2, "c": 3}, + "d", + lambda x: x > 0, + r"Missing required key: d\.", + ), + ( + {"a": "1", "b": 2, "c": 3}, + "a", + lambda x: x > 0, + r"a must be of type int\.", + ), + ( + {"a": -1, "b": 2, "c": 3}, + "a", + lambda x: x > 0, + r"a has invalid value -1\.", + ), + ), + ) + def test_validate_key( + data_: dict, key: str, validator: Callable, expected_error: Optional[str] + ) -> None: + """Test the `_validate_key` method.""" + if expected_error: + with pytest.raises(ValueError, match=expected_error): + AvailabilityWindow._validate_key(data_, key, validator) + else: + AvailabilityWindow._validate_key(data_, key, validator) + + @staticmethod + @pytest.mark.parametrize( + "data_, error_regex", + ( + ("not a dict", r"Expected dict, got"), + ( + {"max_length": -1, "array": 42, "num_positive": 10, "num_negative": 0}, + r"max_length", + ), + ( + {"max_length": 2, "array": 4, "num_positive": 10, "num_negative": 0}, + r"array", + ), + ( + {"max_length": 8, "array": 42, "num_positive": -1, "num_negative": 0}, + r"num_positive", + ), + ( + {"max_length": 8, "array": 42, "num_positive": 10, "num_negative": -1}, + r"num_negative", + ), + ), + ) + def test_validate_negative(data_: dict, error_regex: str) -> None: + """Negative tests for the `_validate` method.""" + with pytest.raises((TypeError, ValueError), match=error_regex): + AvailabilityWindow._validate(data_) + + @staticmethod + @given(availability_window_data()) + def test_validate_positive(data_: Dict[str, int]) -> None: + """Positive tests for the `_validate` method.""" + AvailabilityWindow._validate(data_) + + @staticmethod + @given(availability_window_data()) + def test_from_dict(data_: Dict[str, int]) -> None: + """Test `from_dict` method.""" + availability_window = AvailabilityWindow.from_dict(data_) + + # convert the serialized array to a binary string + binary_number = bin(data_["array"])[2:] + # convert each character in the binary string to a flag + flags = [bool(int(digit)) for digit in binary_number] + expected_window = deque(flags, maxlen=data_["max_length"]) + + assert availability_window._max_length == data_["max_length"] + assert availability_window._window == expected_window + assert availability_window._num_positive == data_["num_positive"] + assert availability_window._num_negative == data_["num_negative"] + + @staticmethod + @given(availability_window_data()) + def test_to_dict_and_back(data_: Dict[str, int]) -> None: + """Test that the `from_dict` produces an object that generates the input data again when calling `to_dict`.""" + availability_window = AvailabilityWindow.from_dict(data_) + assert availability_window.to_dict() == data_ + + +class TestOffenceStatus: + """Test the `OffenceStatus` dataclass.""" + + @staticmethod + @pytest.mark.parametrize("custom_amount", (0, 5)) + @pytest.mark.parametrize("light_unit_amount, serious_unit_amount", ((1, 2),)) + @pytest.mark.parametrize( + "validator_downtime, invalid_payload, blacklisted, suspected, " + "num_unknown_offenses, num_double_signed, num_light_client_attack, expected", + ( + (False, False, False, False, 0, 0, 0, 0), + (True, False, False, False, 0, 0, 0, 1), + (False, True, False, False, 0, 0, 0, 1), + (False, False, True, False, 0, 0, 0, 1), + (False, False, False, True, 0, 0, 0, 1), + (False, False, False, False, 1, 0, 0, 2), + (False, False, False, False, 0, 1, 0, 2), + (False, False, False, False, 0, 0, 1, 2), + (False, False, False, False, 0, 2, 1, 6), + (False, True, False, True, 5, 2, 1, 18), + (True, True, True, True, 5, 2, 1, 20), + ), + ) + def test_slash_amount( + custom_amount: int, + light_unit_amount: int, + serious_unit_amount: int, + validator_downtime: bool, + invalid_payload: bool, + blacklisted: bool, + suspected: bool, + num_unknown_offenses: int, + num_double_signed: int, + num_light_client_attack: int, + expected: int, + ) -> None: + """Test the `slash_amount` method.""" + status = OffenceStatus() + + if validator_downtime: + for _ in range(abci_base.NUMBER_OF_BLOCKS_TRACKED): + status.validator_downtime.add(True) + + for _ in range(abci_base.NUMBER_OF_ROUNDS_TRACKED): + if invalid_payload: + status.invalid_payload.add(True) + if blacklisted: + status.blacklisted.add(True) + if suspected: + status.suspected.add(True) + + status.num_unknown_offenses = num_unknown_offenses + status.num_double_signed = num_double_signed + status.num_light_client_attack = num_light_client_attack + status.custom_offences_amount = custom_amount + + actual = status.slash_amount(light_unit_amount, serious_unit_amount) + assert actual == expected + status.custom_offences_amount + + +@composite +def offence_tracking(draw: DrawFn) -> Tuple[Evidences, LastCommitInfo]: + """A strategy for building offences reported by Tendermint.""" + n_validators = draw(integers(min_value=1, max_value=10)) + + validators = [ + draw( + builds( + Validator, + address=just(bytes(i)), + power=integers(min_value=0), + ) + ) + for i in range(n_validators) + ] + + evidences = builds( + Evidences, + byzantine_validators=lists( + builds( + Evidence, + evidence_type=sampled_from(EvidenceType), + validator=sampled_from(validators), + height=integers(min_value=0), + time=builds( + Timestamp, + seconds=integers(min_value=0), + nanos=integers(min_value=0, max_value=999_999_999), + ), + total_voting_power=integers(min_value=0), + ), + min_size=n_validators, + max_size=n_validators, + unique_by=lambda v: v.validator.address, + ), + ) + + last_commit_info = builds( + LastCommitInfo, + round_=integers(min_value=0), + votes=lists( + builds( + VoteInfo, + validator=sampled_from(validators), + signed_last_block=booleans(), + ), + min_size=n_validators, + max_size=n_validators, + unique_by=lambda v: v.validator.address, + ), + ) + + ev_example, commit_example = draw(evidences), draw(last_commit_info) + + # this assertion proves that all the validators are unique + unique_commit_addresses = set( + v.validator.address.decode() for v in commit_example.votes + ) + assert len(unique_commit_addresses) == n_validators + + # this assertion proves that the same validators are used for evidences and votes + assert unique_commit_addresses == set( + e.validator.address.decode() for e in ev_example.byzantine_validators + ) + + return ev_example, commit_example + + +@composite +def offence_status(draw: DrawFn) -> OffenceStatus: + """Build an offence status instance.""" + validator_downtime = just( + AvailabilityWindow.from_dict(draw(availability_window_data())) + ) + invalid_payload = just( + AvailabilityWindow.from_dict(draw(availability_window_data())) + ) + blacklisted = just(AvailabilityWindow.from_dict(draw(availability_window_data()))) + suspected = just(AvailabilityWindow.from_dict(draw(availability_window_data()))) + + status = builds( + OffenceStatus, + validator_downtime=validator_downtime, + invalid_payload=invalid_payload, + blacklisted=blacklisted, + suspected=suspected, + num_unknown_offenses=integers(min_value=0), + num_double_signed=integers(min_value=0), + num_light_client_attack=integers(min_value=0), + ) + + return draw(status) + + +class TestOffenseStatusEncoderDecoder: + """Test the `OffenseStatusEncoder` and the `OffenseStatusDecoder`.""" + + @staticmethod + @given(dictionaries(keys=text(), values=offence_status(), min_size=1)) + def test_encode_decode_offense_status(offense_status: str) -> None: + """Test encoding an offense status mapping and then decoding it by using the custom encoder/decoder.""" + encoded = json.dumps(offense_status, cls=OffenseStatusEncoder) + decoded = json.loads(encoded, cls=OffenseStatusDecoder) + + assert decoded == offense_status + + def test_encode_unknown(self) -> None: + """Test the encoder with an unknown input.""" + + class Unknown: + """A dummy class that the encoder is not aware of.""" + + unknown = "?" + + with pytest.raises( + TypeError, match="Object of type Unknown is not JSON serializable" + ): + json.dumps(Unknown(), cls=OffenseStatusEncoder) + + +class TestRoundSequence: + """Test the RoundSequence class.""" + + def setup(self) -> None: + """Set up the test.""" + self.round_sequence = RoundSequence( + context=MagicMock(), abci_app_cls=AbciAppTest + ) + self.round_sequence.setup(MagicMock(), logging.getLogger()) + self.round_sequence.tm_height = 1 + + @pytest.mark.parametrize( + "property_name, set_twice_exc, config_exc", + ( + ( + "validator_to_agent", + "The mapping of the validators' addresses to their agent addresses can only be set once. " + "Attempted to set with {new_content_attempt} but it has content already: {value}.", + "The mapping of the validators' addresses to their agent addresses has not been set.", + ), + ), + ) + @given(data()) + def test_slashing_properties( + self, property_name: str, set_twice_exc: str, config_exc: str, _data: Any + ) -> None: + """Test `validator_to_agent` getter and setter.""" + if property_name == "validator_to_agent": + data_generator = dictionaries(text(), text()) + else: + data_generator = dictionaries(text(), just(OffenceStatus())) + + value = _data.draw(data_generator) + round_sequence = RoundSequence(context=MagicMock(), abci_app_cls=AbciAppTest) + + if value: + setattr(round_sequence, property_name, value) + assert getattr(round_sequence, property_name) == value + new_content_attempt = _data.draw(data_generator) + with pytest.raises( + ValueError, + match=re.escape( + set_twice_exc.format( + new_content_attempt=new_content_attempt, value=value + ) + ), + ): + setattr(round_sequence, property_name, new_content_attempt) + return + + with pytest.raises(SlashingNotConfiguredError, match=config_exc): + getattr(round_sequence, property_name) + + @mock.patch("json.loads", return_value="json_serializable") + @pytest.mark.parametrize("slashing_config", (None, "", "test")) + def test_sync_db_and_slashing( + self, mock_loads: mock.MagicMock, slashing_config: str + ) -> None: + """Test the `sync_db_and_slashing` method.""" + self.round_sequence.latest_synchronized_data.slashing_config = slashing_config + serialized_db_state = "dummy_db_state" + self.round_sequence.sync_db_and_slashing(serialized_db_state) + + # Check that `sync()` was called with the correct arguments + mock_sync = cast( + mock.Mock, self.round_sequence.abci_app.synchronized_data.db.sync + ) + mock_sync.assert_called_once_with(serialized_db_state) + + if slashing_config: + mock_loads.assert_called_once_with( + slashing_config, cls=OffenseStatusDecoder + ) + else: + mock_loads.assert_not_called() + + @mock.patch("json.dumps") + @pytest.mark.parametrize("slashing_enabled", (True, False)) + def test_store_offence_status( + self, mock_dumps: mock.MagicMock, slashing_enabled: bool + ) -> None: + """Test the `store_offence_status` method.""" + # Set up mock objects and return values + self.round_sequence._offence_status = {"not_encoded": OffenceStatus()} + mock_encoded_status = "encoded_status" + mock_dumps.return_value = mock_encoded_status + + self.round_sequence._slashing_enabled = slashing_enabled + + # Call the method to be tested + self.round_sequence.store_offence_status() + + if slashing_enabled: + # Check that `json.dumps()` was called with the correct arguments, only if slashing is enabled + mock_dumps.assert_called_once_with( + self.round_sequence.offence_status, + cls=OffenseStatusEncoder, + sort_keys=True, + ) + assert ( + self.round_sequence.abci_app.synchronized_data.db.slashing_config + == mock_encoded_status + ) + return + + # otherwise check that it was not called + mock_dumps.assert_not_called() + + @given( + validator=builds(Validator, address=binary(), power=integers()), + agent_address=text(), + ) + def test_get_agent_address(self, validator: Validator, agent_address: str) -> None: + """Test `get_agent_address` method.""" + round_sequence = RoundSequence(context=MagicMock(), abci_app_cls=AbciAppTest) + round_sequence.validator_to_agent = { + validator.address.hex().upper(): agent_address + } + assert round_sequence.get_agent_address(validator) == agent_address + + unknown = deepcopy(validator) + unknown.address += b"unknown" + with pytest.raises( + ValueError, + match=re.escape( + f"Requested agent address for an unknown validator address {unknown.address.hex().upper()}. " + f"Available validators are: {round_sequence.validator_to_agent.keys()}" + ), + ): + round_sequence.get_agent_address(unknown) + + @pytest.mark.parametrize("offset", tuple(range(5))) + @pytest.mark.parametrize("n_blocks", (0, 1, 10)) + def test_height(self, n_blocks: int, offset: int) -> None: + """Test 'height' property.""" + self.round_sequence._blockchain._blocks = [MagicMock() for _ in range(n_blocks)] + self.round_sequence._blockchain._height_offset = offset + assert self.round_sequence._blockchain.length == n_blocks + assert self.round_sequence.height == n_blocks + offset + + def test_is_finished(self) -> None: + """Test 'is_finished' property.""" + assert not self.round_sequence.is_finished + self.round_sequence.abci_app._current_round = None + assert self.round_sequence.is_finished + + def test_last_round(self) -> None: + """Test 'last_round' property.""" + assert self.round_sequence.last_round_id is None + + def test_last_timestamp_none(self) -> None: + """ + Test 'last_timestamp' property. + + The property is None because there are no blocks. + """ + with pytest.raises(ABCIAppInternalError, match="last timestamp is None"): + self.round_sequence.last_timestamp + + def test_last_timestamp(self) -> None: + """Test 'last_timestamp' property, positive case.""" + seconds = 1 + nanoseconds = 1000 + expected_timestamp = datetime.datetime.fromtimestamp( + seconds + nanoseconds / 10**9 + ) + self.round_sequence._blockchain.add_block( + Block(MagicMock(height=1, timestamp=expected_timestamp), []) + ) + assert self.round_sequence.last_timestamp == expected_timestamp + + def test_abci_app_negative(self) -> None: + """Test 'abci_app' property, negative case.""" + self.round_sequence._abci_app = None + with pytest.raises(ABCIAppInternalError, match="AbciApp not set"): + self.round_sequence.abci_app + + def test_check_is_finished_negative(self) -> None: + """Test 'check_is_finished', negative case.""" + self.round_sequence.abci_app._current_round = None + with pytest.raises( + ValueError, + match="round sequence is finished, cannot accept new transactions", + ): + self.round_sequence.check_is_finished() + + def test_current_round_positive(self) -> None: + """Test 'current_round' property getter, positive case.""" + assert isinstance(self.round_sequence.current_round, ConcreteRoundA) + + def test_current_round_negative_current_round_not_set(self) -> None: + """Test 'current_round' property getter, negative case (current round not set).""" + self.round_sequence.abci_app._current_round = None + with pytest.raises(ValueError, match="current_round not set!"): + self.round_sequence.current_round + + def test_current_round_id(self) -> None: + """Test 'current_round_id' property getter""" + assert self.round_sequence.current_round_id == ConcreteRoundA.auto_round_id() + + def test_latest_result(self) -> None: + """Test 'latest_result' property getter.""" + assert self.round_sequence.latest_synchronized_data + + @pytest.mark.parametrize("committed", (True, False)) + def test_last_round_transition_timestamp(self, committed: bool) -> None: + """Test 'last_round_transition_timestamp' method.""" + if committed: + self.round_sequence.begin_block( + MagicMock(height=1), MagicMock(), MagicMock() + ) + self.round_sequence.end_block() + self.round_sequence.commit() + assert ( + self.round_sequence.last_round_transition_timestamp + == self.round_sequence._blockchain.last_block.timestamp + ) + else: + assert self.round_sequence._blockchain.height == 0 + with pytest.raises( + ValueError, + match="Trying to access `last_round_transition_timestamp` while no transition has been completed yet.", + ): + _ = self.round_sequence.last_round_transition_timestamp + + @pytest.mark.parametrize("committed", (True, False)) + def test_last_round_transition_height(self, committed: bool) -> None: + """Test 'last_round_transition_height' method.""" + if committed: + self.round_sequence.begin_block( + MagicMock(height=1), MagicMock(), MagicMock() + ) + self.round_sequence.end_block() + self.round_sequence.commit() + assert ( + self.round_sequence.last_round_transition_height + == self.round_sequence._blockchain.height + == 1 + ) + else: + assert self.round_sequence._blockchain.height == 0 + with pytest.raises( + ValueError, + match="Trying to access `last_round_transition_height` while no transition has been completed yet.", + ): + _ = self.round_sequence.last_round_transition_height + + def test_block_before_blockchain_is_init(self, caplog: LogCaptureFixture) -> None: + """Test block received before blockchain initialized.""" + + self.round_sequence.begin_block(MagicMock(height=1), MagicMock(), MagicMock()) + self.round_sequence.end_block() + blockchain = self.round_sequence.blockchain + blockchain._is_init = False + self.round_sequence.blockchain = blockchain + with caplog.at_level(logging.INFO): + self.round_sequence.commit() + expected = "Received block with height 1 before the blockchain was initialized." + assert expected in caplog.text + + @pytest.mark.parametrize("last_round_transition_root_hash", (b"", b"test")) + def test_last_round_transition_root_hash( + self, + last_round_transition_root_hash: bytes, + ) -> None: + """Test 'last_round_transition_root_hash' method.""" + self.round_sequence._last_round_transition_root_hash = ( + last_round_transition_root_hash + ) + + if last_round_transition_root_hash == b"": + with mock.patch.object( + RoundSequence, + "root_hash", + new_callable=mock.PropertyMock, + return_value="test", + ): + assert self.round_sequence.last_round_transition_root_hash == "test" + else: + assert ( + self.round_sequence.last_round_transition_root_hash + == last_round_transition_root_hash + ) + + @pytest.mark.parametrize("tm_height", (None, 1, 5)) + def test_last_round_transition_tm_height(self, tm_height: Optional[int]) -> None: + """Test 'last_round_transition_tm_height' method.""" + if tm_height is None: + with pytest.raises( + ValueError, + match="Trying to access Tendermint's last round transition height before any `end_block` calls.", + ): + _ = self.round_sequence.last_round_transition_tm_height + else: + self.round_sequence.tm_height = tm_height + self.round_sequence.begin_block( + MagicMock(height=1), MagicMock(), MagicMock() + ) + self.round_sequence.end_block() + self.round_sequence.commit() + assert self.round_sequence.last_round_transition_tm_height == tm_height + + @given(one_of(none(), integers())) + def test_tm_height(self, tm_height: int) -> None: + """Test `tm_height` getter and setter.""" + + self.round_sequence.tm_height = tm_height + + if tm_height is None: + with pytest.raises( + ValueError, + match="Trying to access Tendermint's current height before any `end_block` calls.", + ): + _ = self.round_sequence.tm_height + else: + assert ( + self.round_sequence.tm_height + == self.round_sequence._tm_height + == tm_height + ) + + @given(one_of(none(), datetimes())) + def test_block_stall_deadline_expired( + self, block_stall_deadline: datetime.datetime + ) -> None: + """Test 'block_stall_deadline_expired' method.""" + + self.round_sequence._block_stall_deadline = block_stall_deadline + actual = self.round_sequence.block_stall_deadline_expired + + if block_stall_deadline is None: + assert actual is False + else: + expected = datetime.datetime.now() > block_stall_deadline + assert actual is expected + + @pytest.mark.parametrize("begin_height", tuple(range(0, 50, 10))) + @pytest.mark.parametrize("initial_height", tuple(range(0, 11, 5))) + def test_init_chain(self, begin_height: int, initial_height: int) -> None: + """Test 'init_chain' method.""" + for i in range(begin_height): + self.round_sequence._blockchain.add_block( + MagicMock(header=MagicMock(height=i + 1)) + ) + assert self.round_sequence._blockchain.height == begin_height + self.round_sequence.init_chain(initial_height) + assert self.round_sequence._blockchain.height == initial_height - 1 + + @given(offence_tracking()) + @settings(suppress_health_check=[HealthCheck.too_slow]) + def test_track_tm_offences( + self, offences: Tuple[Evidences, LastCommitInfo] + ) -> None: + """Test `_track_tm_offences` method.""" + evidences, last_commit_info = offences + dummy_addr_template = "agent_{i}" + round_sequence = RoundSequence(context=MagicMock(), abci_app_cls=AbciAppTest) + synchronized_data_mock = MagicMock() + round_sequence.setup(synchronized_data_mock, MagicMock()) + round_sequence.enable_slashing() + + expected_offence_status = { + dummy_addr_template.format(i=i): OffenceStatus() + for i in range(len(last_commit_info.votes)) + } + for i, vote_info in enumerate(last_commit_info.votes): + agent_address = dummy_addr_template.format(i=i) + # initialize dummy round sequence's offence status and validator to agent address mapping + round_sequence._offence_status[agent_address] = OffenceStatus() + validator_address = vote_info.validator.address.hex() + round_sequence._validator_to_agent[validator_address] = agent_address + # set expected result + expected_was_down = not vote_info.signed_last_block + expected_offence_status[agent_address].validator_downtime.add( + expected_was_down + ) + + for byzantine_validator in evidences.byzantine_validators: + agent_address = round_sequence._validator_to_agent[ + byzantine_validator.validator.address.hex() + ] + evidence_type = byzantine_validator.evidence_type + expected_offence_status[agent_address].num_unknown_offenses += bool( + evidence_type == EvidenceType.UNKNOWN + ) + expected_offence_status[agent_address].num_double_signed += bool( + evidence_type == EvidenceType.DUPLICATE_VOTE + ) + expected_offence_status[agent_address].num_light_client_attack += bool( + evidence_type == EvidenceType.LIGHT_CLIENT_ATTACK + ) + + round_sequence._track_tm_offences(evidences, last_commit_info) + assert round_sequence._offence_status == expected_offence_status + + @mock.patch.object(abci_base, "ADDRESS_LENGTH", len("agent_i")) + def test_track_app_offences(self) -> None: + """Test `_track_app_offences` method.""" + dummy_addr_template = "agent_{i}" + stub_offending_keepers = [dummy_addr_template.format(i=i) for i in range(2)] + self.round_sequence.enable_slashing() + self.round_sequence._offence_status = { + dummy_addr_template.format(i=i): OffenceStatus() for i in range(4) + } + expected_offence_status = deepcopy(self.round_sequence._offence_status) + + for i in (dummy_addr_template.format(i=i) for i in range(4)): + offended = i in stub_offending_keepers + expected_offence_status[i].blacklisted.add(offended) + expected_offence_status[i].suspected.add(offended) + + with mock.patch.object( + self.round_sequence.latest_synchronized_data.db, + "get", + return_value="".join(stub_offending_keepers), + ): + self.round_sequence._track_app_offences() + assert self.round_sequence._offence_status == expected_offence_status + + @given(builds(SlashingNotConfiguredError, text())) + def test_handle_slashing_not_configured( + self, exc: SlashingNotConfiguredError + ) -> None: + """Test `_handle_slashing_not_configured` method.""" + logging.disable(logging.CRITICAL) + + round_sequence = RoundSequence(context=MagicMock(), abci_app_cls=AbciAppTest) + round_sequence.setup(MagicMock(), MagicMock()) + + assert not round_sequence._slashing_enabled + assert round_sequence.latest_synchronized_data.nb_participants == 0 + round_sequence._handle_slashing_not_configured(exc) + assert not round_sequence._slashing_enabled + + with mock.patch.object( + round_sequence.latest_synchronized_data.db, + "get", + return_value=[i for i in range(4)], + ): + assert round_sequence.latest_synchronized_data.nb_participants == 4 + round_sequence._handle_slashing_not_configured(exc) + assert not round_sequence._slashing_enabled + + logging.disable(logging.NOTSET) + + @pytest.mark.parametrize("_track_offences_raises", (True, False)) + def test_try_track_offences(self, _track_offences_raises: bool) -> None: + """Test `_try_track_offences` method.""" + evidences, last_commit_info = MagicMock(), MagicMock() + self.round_sequence.enable_slashing() + with mock.patch.object( + self.round_sequence, + "_track_app_offences", + ), mock.patch.object( + self.round_sequence, + "_track_tm_offences", + side_effect=SlashingNotConfiguredError if _track_offences_raises else None, + ) as _track_offences_mock, mock.patch.object( + self.round_sequence, "_handle_slashing_not_configured" + ) as _handle_slashing_not_configured_mock: + self.round_sequence._try_track_offences(evidences, last_commit_info) + if _track_offences_raises: + _handle_slashing_not_configured_mock.assert_called_once() + else: + _track_offences_mock.assert_called_once_with( + evidences, last_commit_info + ) + + def test_begin_block_negative_is_finished(self) -> None: + """Test 'begin_block' method, negative case (round sequence is finished).""" + self.round_sequence.abci_app._current_round = None + with pytest.raises( + ABCIAppInternalError, + match="internal error: round sequence is finished, cannot accept new blocks", + ): + self.round_sequence.begin_block(MagicMock(), MagicMock(), MagicMock()) + + def test_begin_block_negative_wrong_phase(self) -> None: + """Test 'begin_block' method, negative case (wrong phase).""" + self.round_sequence._block_construction_phase = MagicMock() + with pytest.raises( + ABCIAppInternalError, + match="internal error: cannot accept a 'begin_block' request.", + ): + self.round_sequence.begin_block(MagicMock(), MagicMock(), MagicMock()) + + def test_begin_block_positive(self) -> None: + """Test 'begin_block' method, positive case.""" + self.round_sequence.begin_block(MagicMock(), MagicMock(), MagicMock()) + + def test_deliver_tx_negative_wrong_phase(self) -> None: + """Test 'begin_block' method, negative (wrong phase).""" + with pytest.raises( + ABCIAppInternalError, + match="internal error: cannot accept a 'deliver_tx' request", + ): + self.round_sequence.deliver_tx(MagicMock()) + + def test_deliver_tx_positive_not_valid(self) -> None: + """Test 'begin_block' method, positive (not valid).""" + self.round_sequence.begin_block(MagicMock(), MagicMock(), MagicMock()) + with mock.patch.object( + self.round_sequence.current_round, "check_transaction", return_value=True + ): + with mock.patch.object( + self.round_sequence.current_round, "process_transaction" + ): + self.round_sequence.deliver_tx(MagicMock()) + + def test_end_block_negative_wrong_phase(self) -> None: + """Test 'end_block' method, negative case (wrong phase).""" + with pytest.raises( + ABCIAppInternalError, + match="internal error: cannot accept a 'end_block' request.", + ): + self.round_sequence.end_block() + + def test_end_block_positive(self) -> None: + """Test 'end_block' method, positive case.""" + self.round_sequence.begin_block(MagicMock(), MagicMock(), MagicMock()) + self.round_sequence.end_block() + + def test_commit_negative_wrong_phase(self) -> None: + """Test 'end_block' method, negative case (wrong phase).""" + with pytest.raises( + ABCIAppInternalError, + match="internal error: cannot accept a 'commit' request.", + ): + self.round_sequence.commit() + + def test_commit_negative_exception(self) -> None: + """Test 'end_block' method, negative case (raise exception).""" + self.round_sequence.begin_block(MagicMock(height=1), MagicMock(), MagicMock()) + self.round_sequence.end_block() + with mock.patch.object( + self.round_sequence._blockchain, "add_block", side_effect=AddBlockError + ): + with pytest.raises(AddBlockError): + self.round_sequence.commit() + + def test_commit_positive_no_change_round(self) -> None: + """Test 'end_block' method, positive (no change round).""" + self.round_sequence.begin_block(MagicMock(height=1), MagicMock(), MagicMock()) + self.round_sequence.end_block() + with mock.patch.object( + self.round_sequence.current_round, + "end_block", + return_value=None, + ): + assert isinstance(self.round_sequence.current_round, ConcreteRoundA) + + def test_commit_positive_with_change_round(self) -> None: + """Test 'end_block' method, positive (with change round).""" + self.round_sequence.begin_block(MagicMock(height=1), MagicMock(), MagicMock()) + self.round_sequence.end_block() + round_result, next_round = MagicMock(), MagicMock() + with mock.patch.object( + self.round_sequence.current_round, + "end_block", + return_value=(round_result, next_round), + ): + self.round_sequence.commit() + assert not isinstance( + self.round_sequence.abci_app._current_round, ConcreteRoundA + ) + assert self.round_sequence.latest_synchronized_data == round_result + + @pytest.mark.parametrize("is_replay", (True, False)) + def test_reset_blockchain(self, is_replay: bool) -> None: + """Test `reset_blockchain` method.""" + self.round_sequence.reset_blockchain(is_replay) + if is_replay: + assert ( + self.round_sequence._block_construction_phase + == RoundSequence._BlockConstructionState.WAITING_FOR_BEGIN_BLOCK + ) + assert self.round_sequence._blockchain.height == 0 + + def last_round_values_updated(self, any_: bool = True) -> bool: + """Check if the values for the last round-related attributes have been updated.""" + seq = self.round_sequence + + current_last_pairs = ( + ( + seq._blockchain.last_block.timestamp, + seq._last_round_transition_timestamp, + ), + (seq._blockchain.height, seq._last_round_transition_height), + (seq.root_hash, seq._last_round_transition_root_hash), + (seq.tm_height, seq._last_round_transition_tm_height), + ) + + if any_: + return any(current == last for current, last in current_last_pairs) + + return all(current == last for current, last in current_last_pairs) + + @mock.patch.object(AbciApp, "process_event") + @mock.patch.object(RoundSequence, "serialized_offence_status") + @pytest.mark.parametrize("end_block_res", (None, (MagicMock(), MagicMock()))) + @pytest.mark.parametrize( + "slashing_enabled, offence_status_", + ( + ( + False, + False, + ), + ( + False, + True, + ), + ( + False, + False, + ), + ( + True, + True, + ), + ), + ) + def test_update_round( + self, + serialized_offence_status_mock: mock.Mock, + process_event_mock: mock.Mock, + end_block_res: Optional[Tuple[BaseSynchronizedData, Any]], + slashing_enabled: bool, + offence_status_: dict, + ) -> None: + """Test '_update_round' method.""" + self.round_sequence.begin_block(MagicMock(height=1), MagicMock(), MagicMock()) + block = self.round_sequence._block_builder.get_block() + self.round_sequence._blockchain.add_block(block) + self.round_sequence._slashing_enabled = slashing_enabled + self.round_sequence._offence_status = offence_status_ + + with mock.patch.object( + self.round_sequence.current_round, "end_block", return_value=end_block_res + ): + self.round_sequence._update_round() + + if end_block_res is None: + assert not self.last_round_values_updated() + process_event_mock.assert_not_called() + return + + assert self.last_round_values_updated(any_=False) + process_event_mock.assert_called_with( + end_block_res[-1], result=end_block_res[0] + ) + + if slashing_enabled: + serialized_offence_status_mock.assert_called_once() + else: + serialized_offence_status_mock.assert_not_called() + + @mock.patch.object(AbciApp, "process_event") + @pytest.mark.parametrize( + "termination_round_result, current_round_result", + [ + (None, None), + (None, (MagicMock(), MagicMock())), + ((MagicMock(), MagicMock()), None), + ((MagicMock(), MagicMock()), (MagicMock(), MagicMock())), + ], + ) + def test_update_round_when_termination_returns( + self, + process_event_mock: mock.Mock, + termination_round_result: Optional[Tuple[BaseSynchronizedData, Any]], + current_round_result: Optional[Tuple[BaseSynchronizedData, Any]], + ) -> None: + """Test '_update_round' method.""" + self.round_sequence.begin_block(MagicMock(height=1), MagicMock(), MagicMock()) + block = self.round_sequence._block_builder.get_block() + self.round_sequence._blockchain.add_block(block) + self.round_sequence.abci_app.add_background_app(STUB_TERMINATION_CONFIG) + self.round_sequence.abci_app.setup() + + with mock.patch.object( + self.round_sequence.current_round, + "end_block", + return_value=current_round_result, + ), mock.patch.object( + ConcreteBackgroundRound, + "end_block", + return_value=termination_round_result, + ): + self.round_sequence._update_round() + + if termination_round_result is None and current_round_result is None: + assert ( + self.round_sequence._last_round_transition_timestamp + != self.round_sequence._blockchain.last_block.timestamp + ) + assert ( + self.round_sequence._last_round_transition_height + != self.round_sequence._blockchain.height + ) + assert ( + self.round_sequence._last_round_transition_root_hash + != self.round_sequence.root_hash + ) + assert ( + self.round_sequence._last_round_transition_tm_height + != self.round_sequence.tm_height + ) + process_event_mock.assert_not_called() + elif termination_round_result is None and current_round_result is not None: + assert ( + self.round_sequence._last_round_transition_timestamp + == self.round_sequence._blockchain.last_block.timestamp + ) + assert ( + self.round_sequence._last_round_transition_height + == self.round_sequence._blockchain.height + ) + assert ( + self.round_sequence._last_round_transition_root_hash + == self.round_sequence.root_hash + ) + assert ( + self.round_sequence._last_round_transition_tm_height + == self.round_sequence.tm_height + ) + process_event_mock.assert_called_with( + current_round_result[-1], + result=current_round_result[0], + ) + elif termination_round_result is not None: + assert ( + self.round_sequence._last_round_transition_timestamp + == self.round_sequence._blockchain.last_block.timestamp + ) + assert ( + self.round_sequence._last_round_transition_height + == self.round_sequence._blockchain.height + ) + assert ( + self.round_sequence._last_round_transition_root_hash + == self.round_sequence.root_hash + ) + assert ( + self.round_sequence._last_round_transition_tm_height + == self.round_sequence.tm_height + ) + process_event_mock.assert_called_with( + termination_round_result[-1], + result=termination_round_result[0], + ) + + self.round_sequence.abci_app.background_apps.clear() + + @pytest.mark.parametrize("restart_from_round", (ConcreteRoundA, MagicMock())) + @pytest.mark.parametrize("serialized_db_state", (None, "serialized state")) + @given(integers()) + def test_reset_state( + self, + restart_from_round: AbstractRound, + serialized_db_state: str, + round_count: int, + ) -> None: + """Tests reset_state""" + with mock.patch.object( + self.round_sequence, + "_reset_to_default_params", + ) as mock_reset, mock.patch.object( + self.round_sequence, "sync_db_and_slashing" + ) as mock_sync_db_and_slashing: + transition_fn = self.round_sequence.abci_app.transition_function + round_id = restart_from_round.auto_round_id() + if restart_from_round in transition_fn: + self.round_sequence.reset_state( + round_id, round_count, serialized_db_state + ) + mock_reset.assert_called() + + if serialized_db_state is None: + mock_sync_db_and_slashing.assert_not_called() + + else: + mock_sync_db_and_slashing.assert_called_once_with( + serialized_db_state + ) + assert ( + self.round_sequence._last_round_transition_root_hash + == self.round_sequence.root_hash + ) + + else: + round_ids = {cls.auto_round_id() for cls in transition_fn} + with pytest.raises( + ABCIAppInternalError, + match=re.escape( + "internal error: Cannot reset state. The Tendermint recovery parameters are incorrect. " + "Did you update the `restart_from_round` with an incorrect round id? " + f"Found {round_id}, but the app's transition function has the following round ids: " + f"{round_ids}.", + ), + ): + self.round_sequence.reset_state( + restart_from_round.auto_round_id(), + round_count, + serialized_db_state, + ) + + def test_reset_to_default_params(self) -> None: + """Tests _reset_to_default_params.""" + # we set some values to the parameters, to make sure that they are not "empty" + self.round_sequence._last_round_transition_timestamp = MagicMock() + self.round_sequence._last_round_transition_height = MagicMock() + self.round_sequence._last_round_transition_root_hash = MagicMock() + self.round_sequence._last_round_transition_tm_height = MagicMock() + self.round_sequence._tm_height = MagicMock() + self._pending_offences = MagicMock() + self._slashing_enabled = MagicMock() + + # we reset them + self.round_sequence._reset_to_default_params() + + # we check whether they have been reset + assert self.round_sequence._last_round_transition_timestamp is None + assert self.round_sequence._last_round_transition_height == 0 + assert self.round_sequence._last_round_transition_root_hash == b"" + assert self.round_sequence._last_round_transition_tm_height is None + assert self.round_sequence._tm_height is None + assert self.round_sequence.pending_offences == set() + assert not self.round_sequence._slashing_enabled + + def test_add_pending_offence(self) -> None: + """Tests add_pending_offence.""" + assert self.round_sequence.pending_offences == set() + mock_offence = MagicMock() + self.round_sequence.add_pending_offence(mock_offence) + assert self.round_sequence.pending_offences == {mock_offence} + + +def test_meta_abci_app_when_instance_not_subclass_of_abstract_round() -> None: + """ + Test instantiation of meta-class when instance not a subclass of AbciApp. + + Since the class is not a subclass of AbciApp, the checks performed by + the meta-class should not apply. + """ + + class MyAbciApp(metaclass=_MetaAbciApp): + pass + + +def test_meta_abci_app_when_final_round_not_subclass_of_degenerate_round() -> None: + """Test instantiation of meta-class when a final round is not a subclass of DegenerateRound.""" + + class FinalRound(AbstractRound, ABC): + """A round class for testing.""" + + payload_class = MagicMock() + synchronized_data_class = MagicMock() + payload_attribute = MagicMock() + round_id = "final_round" + + with pytest.raises( + AEAEnforceError, + match="non-final state.*must have at least one non-timeout transition", + ): + + class MyAbciApp(AbciApp, metaclass=_MetaAbciApp): + initial_round_cls: Type[AbstractRound] = ConcreteRoundA + transition_function: Dict[ + Type[AbstractRound], Dict[str, Type[AbstractRound]] + ] = { + ConcreteRoundA: {"event": FinalRound, "timeout": ConcreteRoundA}, + FinalRound: {}, + } + event_to_timeout = {"timeout": 1.0} + final_states: Set[AppState] = set() + + +def test_synchronized_data_type_on_abci_app_init(caplog: LogCaptureFixture) -> None: + """Test synchronized data access""" + + # NOTE: the synchronized data of a particular AbciApp is only + # updated at the end of a round. However, we want to make sure + # that the instance during the first round of any AbciApp is + # in fact and instance of the locally defined SynchronizedData + + sentinel = object() + + class SynchronizedData(BaseSynchronizedData): + """SynchronizedData""" + + @property + def dummy_attr(self) -> object: + return sentinel + + # this is how it's setup in SharedState.setup, using BaseSynchronizedData + synchronized_data = BaseSynchronizedData(db=AbciAppDB(setup_data={})) + + with mock.patch.object(AbciAppTest, "initial_round_cls") as m: + m.synchronized_data_class = SynchronizedData + abci_app = AbciAppTest(synchronized_data, logging.getLogger(), MagicMock()) + abci_app.setup() + assert isinstance(abci_app.synchronized_data, SynchronizedData) + assert abci_app.synchronized_data.dummy_attr == sentinel + + +def test_get_name() -> None: + """Test the get_name method.""" + + class SomeObject: + @property + def some_property(self) -> Any: + """Some getter.""" + return object() + + assert get_name(SomeObject.some_property) == "some_property" + with pytest.raises(ValueError, match="1 is not a property"): + get_name(1) + + +@pytest.mark.parametrize( + "sender, accused_agent_address, offense_round, offense_type_value, last_transition_timestamp, time_to_live, custom_amount", + ( + ( + "sender", + "test_address", + 90, + 3, + 10, + 2, + 10, + ), + ), +) +def test_pending_offences_payload( + sender: str, + accused_agent_address: str, + offense_round: int, + offense_type_value: int, + last_transition_timestamp: int, + time_to_live: int, + custom_amount: int, +) -> None: + """Test `PendingOffencesPayload`""" + + payload = abci_base.PendingOffencesPayload( + sender, + accused_agent_address, + offense_round, + offense_type_value, + last_transition_timestamp, + time_to_live, + custom_amount, + ) + + assert payload.id_ + assert payload.round_count == abci_base.ROUND_COUNT_DEFAULT + assert payload.sender == sender + assert payload.accused_agent_address == accused_agent_address + assert payload.offense_round == offense_round + assert payload.offense_type_value == offense_type_value + assert payload.last_transition_timestamp == last_transition_timestamp + assert payload.time_to_live == time_to_live + assert payload.custom_amount == custom_amount + assert payload.data == { + "accused_agent_address": accused_agent_address, + "offense_round": offense_round, + "offense_type_value": offense_type_value, + "last_transition_timestamp": last_transition_timestamp, + "time_to_live": time_to_live, + "custom_amount": custom_amount, + } + + +class TestPendingOffencesRound(BaseRoundTestClass): + """Tests for `PendingOffencesRound`.""" + + _synchronized_data_class = BaseSynchronizedData + + @given( + accused_agent_address=sampled_from(list(get_participants())), + offense_round=integers(min_value=0), + offense_type_value=sampled_from( + [value.value for value in OffenseType.__members__.values()] + ), + last_transition_timestamp=floats( + min_value=timegm(datetime.datetime(1971, 1, 1).utctimetuple()), + max_value=timegm(datetime.datetime(8000, 1, 1).utctimetuple()) - 2000, + ), + time_to_live=floats(min_value=1, max_value=2000), + custom_amount=integers(min_value=0), + ) + def test_run( + self, + accused_agent_address: str, + offense_round: int, + offense_type_value: int, + last_transition_timestamp: float, + time_to_live: float, + custom_amount: int, + ) -> None: + """Run tests.""" + + test_round = abci_base.PendingOffencesRound( + synchronized_data=self.synchronized_data, + context=MagicMock(), + ) + # initialize the offence status + status_initialization = dict.fromkeys(self.participants, OffenceStatus()) + test_round.context.state.round_sequence.offence_status = status_initialization + + # create the actual and expected value + actual = test_round.context.state.round_sequence.offence_status + expected_invalid = offense_type_value == OffenseType.INVALID_PAYLOAD.value + expected_custom_amount = offense_type_value == OffenseType.CUSTOM.value + expected = deepcopy(status_initialization) + + first_payload, *payloads = [ + abci_base.PendingOffencesPayload( + sender, + accused_agent_address, + offense_round, + offense_type_value, + last_transition_timestamp, + time_to_live, + custom_amount, + ) + for sender in self.participants + ] + + test_round.process_payload(first_payload) + assert test_round.collection == {first_payload.sender: first_payload} + test_round.end_block() + assert actual == expected + + for payload in payloads: + test_round.process_payload(payload) + test_round.end_block() + + expected[accused_agent_address].invalid_payload.add(expected_invalid) + if expected_custom_amount: + expected[accused_agent_address].custom_offences_amount += custom_amount + + assert actual == expected diff --git a/packages/valory/skills/abstract_round_abci/tests/test_base_rounds.py b/packages/valory/skills/abstract_round_abci/tests/test_base_rounds.py new file mode 100644 index 0000000..06da258 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/test_base_rounds.py @@ -0,0 +1,668 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test the base round classes.""" + +# pylint: skip-file + +import re +from enum import Enum +from typing import FrozenSet, List, Optional, Tuple, Union, cast +from unittest.mock import MagicMock + +import pytest + +from packages.valory.skills.abstract_round_abci.base import ( + ABCIAppInternalError, + BaseSynchronizedData, + BaseTxPayload, + TransactionNotValidError, +) +from packages.valory.skills.abstract_round_abci.test_tools.rounds import ( + BaseOnlyKeeperSendsRoundTest, + DummyCollectDifferentUntilAllRound, + DummyCollectDifferentUntilThresholdRound, + DummyCollectNonEmptyUntilThresholdRound, + DummyCollectSameUntilAllRound, + DummyCollectSameUntilThresholdRound, + DummyCollectionRound, + DummyEvent, + DummyOnlyKeeperSendsRound, + DummyTxPayload, + DummyVotingRound, + MAX_PARTICIPANTS, + _BaseRoundTestClass, + get_dummy_tx_payloads, +) + + +class TestCollectionRound(_BaseRoundTestClass): + """Test class for CollectionRound.""" + + def setup( + self, + ) -> None: + """Setup test.""" + super().setup() + + self.test_round = DummyCollectionRound( + synchronized_data=self.synchronized_data, context=MagicMock() + ) + + def test_serialized_collection(self) -> None: + """Test `serialized_collection` property.""" + assert self.test_round.serialized_collection == {} + + for payload in self.tx_payloads: + self.test_round.process_payload(payload) + + mcs_key = "packages.valory.skills.abstract_round_abci.test_tools.rounds.DummyTxPayload" + expected = { + f"agent_{i}": { + "_metaclass_registry_key": mcs_key, + "id_": self.tx_payloads[i].id_, + "round_count": self.tx_payloads[i].round_count, + "sender": self.tx_payloads[i].sender, + "value": self.tx_payloads[i].value, + "vote": self.tx_payloads[i].vote, + } + for i in range(4) + } + + assert self.test_round.serialized_collection == expected + + def test_run( + self, + ) -> None: + """Run tests.""" + + round_id = DummyCollectionRound.auto_round_id() + + # collection round may set a flag to allow payments from inactive agents (rejoin) + assert self.test_round._allow_rejoin_payloads is False # default + assert ( + self.test_round.accepting_payloads_from + == self.synchronized_data.participants + ) + self.test_round._allow_rejoin_payloads = True + assert ( + self.test_round.accepting_payloads_from + == self.synchronized_data.all_participants + ) + + first_payload, *_ = self.tx_payloads + self.test_round.process_payload(first_payload) + assert self.test_round.collection[first_payload.sender] == first_payload + + with pytest.raises( + ABCIAppInternalError, + match=f"internal error: sender agent_0 has already sent value for round: {round_id}", + ): + self.test_round.process_payload(first_payload) + + with pytest.raises( + ABCIAppInternalError, + match=re.escape( + "internal error: sender not in list of participants: ['agent_0', 'agent_1', 'agent_2', 'agent_3']" + ), + ): + self.test_round.process_payload(DummyTxPayload("sender", "value")) + + with pytest.raises( + TransactionNotValidError, + match=f"sender agent_0 has already sent value for round: {round_id}", + ): + self.test_round.check_payload(first_payload) + + with pytest.raises( + TransactionNotValidError, + match=re.escape( + "sender not in list of participants: ['agent_0', 'agent_1', 'agent_2', 'agent_3']" + ), + ): + self.test_round.check_payload(DummyTxPayload("sender", "value")) + + self._test_payload_with_wrong_round_count(self.test_round) + + +class TestCollectDifferentUntilAllRound(_BaseRoundTestClass): + """Test class for CollectDifferentUntilAllRound.""" + + def test_run( + self, + ) -> None: + """Run Tests.""" + + test_round = DummyCollectDifferentUntilAllRound( + synchronized_data=self.synchronized_data, + context=MagicMock(), + ) + round_id = DummyCollectDifferentUntilAllRound.auto_round_id() + + first_payload, *payloads = self.tx_payloads + test_round.process_payload(first_payload) + assert not test_round.collection_threshold_reached + + with pytest.raises( + ABCIAppInternalError, + match=f"internal error: sender agent_0 has already sent value for round: {round_id}", + ): + test_round.process_payload(first_payload) + + with pytest.raises( + TransactionNotValidError, + match=f"sender agent_0 has already sent value for round: {round_id}", + ): + test_round.check_payload(first_payload) + + with pytest.raises( + ABCIAppInternalError, + match="internal error: `CollectDifferentUntilAllRound` encountered a value '.*' that already exists.", + ): + object.__setattr__(first_payload, "sender", "other") + test_round.process_payload(first_payload) + + with pytest.raises( + TransactionNotValidError, + match="`CollectDifferentUntilAllRound` encountered a value '.*' that already exists.", + ): + test_round.check_payload(first_payload) + + for payload in payloads: + assert not test_round.collection_threshold_reached + test_round.process_payload(payload) + + assert test_round.collection_threshold_reached + self._test_payload_with_wrong_round_count(test_round) + + +class TestCollectSameUntilAllRound(_BaseRoundTestClass): + """Test class for CollectSameUntilAllRound.""" + + def test_run( + self, + ) -> None: + """Run Tests.""" + + test_round = DummyCollectSameUntilAllRound( + synchronized_data=self.synchronized_data, + context=MagicMock(), + ) + round_id = DummyCollectSameUntilAllRound.auto_round_id() + + first_payload, *payloads = [ + DummyTxPayload( + sender=agent, + value="test", + ) + for agent in sorted(self.participants) + ] + test_round.process_payload(first_payload) + assert not test_round.collection_threshold_reached + + with pytest.raises( + ABCIAppInternalError, + match="1 votes are not enough for `CollectSameUntilAllRound`", + ): + assert test_round.common_payload + + with pytest.raises( + ABCIAppInternalError, + match=f"internal error: sender agent_0 has already sent value for round: {round_id}", + ): + test_round.process_payload(first_payload) + + with pytest.raises( + TransactionNotValidError, + match=f"sender agent_0 has already sent value for round: {round_id}", + ): + test_round.check_payload(first_payload) + + with pytest.raises( + ABCIAppInternalError, + match="internal error: `CollectSameUntilAllRound` encountered a value '.*' " + "which is not the same as the already existing one: '.*'", + ): + bad_payload = DummyTxPayload( + sender="other", + value="other", + ) + test_round.process_payload(bad_payload) + + with pytest.raises( + TransactionNotValidError, + match="`CollectSameUntilAllRound` encountered a value '.*' " + "which is not the same as the already existing one: '.*'", + ): + test_round.check_payload(bad_payload) + + for payload in payloads: + assert not test_round.collection_threshold_reached + test_round.process_payload(payload) + + assert test_round.collection_threshold_reached + assert test_round.common_payload + self._test_payload_with_wrong_round_count(test_round, "test") + + +class TestCollectSameUntilThresholdRound(_BaseRoundTestClass): + """Test CollectSameUntilThresholdRound.""" + + @pytest.mark.parametrize( + "selection_key", + ("dummy_selection_key", tuple(f"dummy_selection_key_{i}" for i in range(2))), + ) + def test_run( + self, + selection_key: Union[str, Tuple[str, ...]], + ) -> None: + """Run tests.""" + + test_round = DummyCollectSameUntilThresholdRound( + synchronized_data=self.synchronized_data, + context=MagicMock(), + ) + test_round.collection_key = "dummy_collection_key" + test_round.selection_key = selection_key + assert test_round.end_block() is None + + first_payload, *payloads = get_dummy_tx_payloads( + self.participants, value="vote" + ) + test_round.process_payload(first_payload) + + assert not test_round.threshold_reached + with pytest.raises(ABCIAppInternalError, match="not enough votes"): + _ = test_round.most_voted_payload + + for payload in payloads: + test_round.process_payload(payload) + + assert test_round.threshold_reached + assert test_round.most_voted_payload == "vote" + + self._test_payload_with_wrong_round_count(test_round) + + test_round.done_event = DummyEvent.DONE + return_value = cast(Tuple[BaseSynchronizedData, Enum], test_round.end_block()) + assert return_value[-1] == test_round.done_event + + test_round.none_event = DummyEvent.NONE + test_round.collection.clear() + payloads = get_dummy_tx_payloads( + self.participants, value=None, is_value_none=True, is_vote_none=True + ) + for payload in payloads: + test_round.process_payload(payload) + assert test_round.most_voted_payload is None + return_value = cast(Tuple[BaseSynchronizedData, Enum], test_round.end_block()) + assert return_value[-1] == test_round.none_event + + test_round.no_majority_event = DummyEvent.NO_MAJORITY + test_round.collection.clear() + for participant in self.participants: + payload = DummyTxPayload(participant, value=participant) + test_round.process_payload(payload) + return_value = cast(Tuple[BaseSynchronizedData, Enum], test_round.end_block()) + assert return_value[-1] == test_round.no_majority_event + + def test_run_with_none( + self, + ) -> None: + """Run tests.""" + + test_round = DummyCollectSameUntilThresholdRound( + synchronized_data=self.synchronized_data, + context=MagicMock(), + ) + + first_payload, *payloads = get_dummy_tx_payloads( + self.participants, + value=None, + is_value_none=True, + ) + test_round.process_payload(first_payload) + + assert not test_round.threshold_reached + with pytest.raises(ABCIAppInternalError, match="not enough votes"): + _ = test_round.most_voted_payload + + for payload in payloads: + test_round.process_payload(payload) + + assert test_round.threshold_reached + assert test_round.most_voted_payload is None + + +class TestOnlyKeeperSendsRound(_BaseRoundTestClass, BaseOnlyKeeperSendsRoundTest): + """Test OnlyKeeperSendsRound.""" + + @pytest.mark.parametrize( + "payload_key", ("dummy_key", tuple(f"dummy_key_{i}" for i in range(2))) + ) + def test_run( + self, + payload_key: Union[str, Tuple[str, ...]], + ) -> None: + """Run tests.""" + + test_round = DummyOnlyKeeperSendsRound( + synchronized_data=self.synchronized_data.update( + most_voted_keeper_address="agent_0" + ), + context=MagicMock(), + ) + + assert test_round.keeper_payload is None + first_payload, *_ = self.tx_payloads + test_round.process_payload(first_payload) + assert test_round.keeper_payload is not None + + with pytest.raises( + ABCIAppInternalError, + match="internal error: keeper already set the payload.", + ): + test_round.process_payload(first_payload) + + with pytest.raises( + ABCIAppInternalError, + match=re.escape( + "internal error: sender not in list of participants: ['agent_0', 'agent_1', 'agent_2', 'agent_3']" + ), + ): + test_round.process_payload(DummyTxPayload(sender="sender", value="sender")) + + with pytest.raises( + ABCIAppInternalError, match="internal error: agent_1 not elected as keeper." + ): + test_round.process_payload(DummyTxPayload(sender="agent_1", value="sender")) + + with pytest.raises( + TransactionNotValidError, match="keeper payload value already set." + ): + test_round.check_payload(first_payload) + + with pytest.raises( + TransactionNotValidError, + match=re.escape( + "sender not in list of participants: ['agent_0', 'agent_1', 'agent_2', 'agent_3']" + ), + ): + test_round.check_payload(DummyTxPayload(sender="sender", value="sender")) + + with pytest.raises( + TransactionNotValidError, match="agent_1 not elected as keeper." + ): + test_round.check_payload(DummyTxPayload(sender="agent_1", value="sender")) + + self._test_payload_with_wrong_round_count(test_round) + + test_round.done_event = DummyEvent.DONE + test_round.payload_key = payload_key + assert test_round.end_block() + + def test_keeper_payload_is_none( + self, + ) -> None: + """Test keeper payload valur set to none.""" + + keeper = "agent_0" + self._complete_run( + self._test_round( + test_round=DummyOnlyKeeperSendsRound( + synchronized_data=self.synchronized_data.update( + most_voted_keeper_address=keeper, + ), + context=MagicMock(), + ), + keeper_payloads=DummyTxPayload(keeper, None), + synchronized_data_update_fn=lambda _synchronized_data, _test_round: _synchronized_data, + synchronized_data_attr_checks=[], + exit_event="FAIL_EVENT", + ) + ) + + +class TestVotingRound(_BaseRoundTestClass): + """Test VotingRound.""" + + def setup_test_voting_round(self) -> DummyVotingRound: + """Setup test voting round""" + return DummyVotingRound( + synchronized_data=self.synchronized_data, + context=MagicMock(), + ) + + def test_vote_count(self) -> None: + """Testing agent vote count""" + test_round = self.setup_test_voting_round() + a, b, c, d = self.participants + for agents, vote in [((a, d), True), ((c,), False), ((b,), None)]: + for payload in get_dummy_tx_payloads(frozenset(agents), vote=vote): + test_round.process_payload(payload) + assert dict(test_round.vote_count) == {True: 2, False: 1, None: 1} + + self._test_payload_with_wrong_round_count(test_round) + + @pytest.mark.parametrize("vote", [True, False, None]) + def test_threshold(self, vote: Optional[bool]) -> None: + """Runs threshold test.""" + + test_round = self.setup_test_voting_round() + test_round.collection_key = "dummy_collection_key" + test_round.done_event = DummyEvent.DONE + test_round.negative_event = DummyEvent.NEGATIVE + test_round.none_event = DummyEvent.NONE + + expected_threshold = { + True: lambda: test_round.positive_vote_threshold_reached, + False: lambda: test_round.negative_vote_threshold_reached, + None: lambda: test_round.none_vote_threshold_reached, + }[vote] + + expected_event = { + True: test_round.done_event, + False: test_round.negative_event, + None: test_round.none_event, + }[vote] + + first_payload, *payloads = get_dummy_tx_payloads(self.participants, vote=vote) + test_round.process_payload(first_payload) + assert test_round.end_block() is None + assert not expected_threshold() + for payload in payloads: + test_round.process_payload(payload) + assert expected_threshold() + return_value = cast(Tuple[BaseSynchronizedData, Enum], test_round.end_block()) + assert return_value[-1] == expected_event + + def test_end_round_no_majority(self) -> None: + """Test end round""" + + test_round = self.setup_test_voting_round() + test_round.no_majority_event = DummyEvent.NO_MAJORITY + for i, participant in enumerate(self.participants): + payload = DummyTxPayload(participant, value=participant, vote=bool(i % 2)) + test_round.process_payload(payload) + return_value = cast(Tuple[BaseSynchronizedData, Enum], test_round.end_block()) + assert return_value[-1] == test_round.no_majority_event + + def test_invalid_vote_payload_count(self) -> None: + """Testing agent vote count with invalid payload.""" + test_round = self.setup_test_voting_round() + a, b, c, d = self.participants + + class InvalidPayload(BaseTxPayload): + """InvalidPayload""" + + def get_dummy_tx_payloads_( + participants: FrozenSet[str], + ) -> List[BaseTxPayload]: + """Returns a list of DummyTxPayload objects.""" + return [InvalidPayload(sender=agent) for agent in sorted(participants)] + + for agents in [(a, d), (c,), (b,)]: + for payload in get_dummy_tx_payloads_(frozenset(agents)): + test_round.process_payload(payload) + + with pytest.raises(ValueError): + test_round.vote_count + + +class TestCollectDifferentUntilThresholdRound(_BaseRoundTestClass): + """Test CollectDifferentUntilThresholdRound.""" + + @pytest.mark.parametrize( + "required_confirmations", (MAX_PARTICIPANTS, MAX_PARTICIPANTS + 1) + ) + def test_run( + self, + required_confirmations: int, + ) -> None: + """Run tests.""" + + test_round = DummyCollectDifferentUntilThresholdRound( + synchronized_data=self.synchronized_data, + context=MagicMock(), + ) + test_round.block_confirmations = 0 + test_round.required_block_confirmations = required_confirmations + test_round.collection_key = "collection_key" + test_round.done_event = 0 + assert ( + test_round.synchronized_data.consensus_threshold <= required_confirmations + ), "Incorrect test parametrization: required confirmations cannot be set with a smalled value than the consensus threshold" + + first_payload, *payloads = get_dummy_tx_payloads(self.participants, vote=False) + test_round.process_payload(first_payload) + + assert not test_round.collection_threshold_reached + for payload in payloads: + test_round.process_payload(payload) + res = test_round.end_block() + assert test_round.block_confirmations <= required_confirmations + assert res is None + assert test_round.collection_threshold_reached + payloads_since_consensus = 2 + confirmations_remaining = required_confirmations - payloads_since_consensus + for _ in range(confirmations_remaining): + res = test_round.end_block() + assert test_round.block_confirmations <= required_confirmations + assert res is None + + res = test_round.end_block() + assert test_round.block_confirmations > required_confirmations + assert res is not None + assert res[1] == test_round.done_event + + assert test_round.collection_threshold_reached + self._test_payload_with_wrong_round_count(test_round) + + def test_end_round(self) -> None: + """Test end round""" + + test_round = DummyCollectDifferentUntilThresholdRound( + synchronized_data=self.synchronized_data, + context=MagicMock(), + ) + test_round.collection_key = "dummy_collection_key" + test_round.done_event = DummyEvent.DONE + + assert test_round.end_block() is None + for participant in self.participants: + payload = DummyTxPayload(participant, value=participant) + test_round.process_payload(payload) + return_value = cast(Tuple[BaseSynchronizedData, Enum], test_round.end_block()) + assert return_value[-1] == test_round.done_event + + +class TestCollectNonEmptyUntilThresholdRound(_BaseRoundTestClass): + """Test `CollectNonEmptyUntilThresholdRound`.""" + + def test_get_non_empty_values(self) -> None: + """Test `_get_non_empty_values`.""" + test_round = DummyCollectNonEmptyUntilThresholdRound( + synchronized_data=self.synchronized_data, + context=MagicMock(), + ) + payloads = get_dummy_tx_payloads(self.participants) + none_payload_idx = 3 + object.__setattr__(payloads[none_payload_idx], "value", None) + for payload in payloads: + test_round.process_payload(payload) + + non_empty_values = test_round._get_non_empty_values() + assert non_empty_values == { + tuple(sorted(self.participants))[i]: (f"agent_{i}", False) + if i != none_payload_idx + else (False,) + for i in range(4) + } + + self._test_payload_with_wrong_round_count(test_round) + + def test_process_payload(self) -> None: + """Test `process_payload`.""" + test_round = DummyCollectNonEmptyUntilThresholdRound( + synchronized_data=self.synchronized_data, + context=MagicMock(), + ) + first_payload, *payloads = get_dummy_tx_payloads(self.participants) + test_round.process_payload(first_payload) + + assert not test_round.collection_threshold_reached + for payload in payloads: + test_round.process_payload(payload) + + assert test_round.collection_threshold_reached + + @pytest.mark.parametrize( + "selection_key", + ("dummy_selection_key", tuple(f"dummy_selection_key_{i}" for i in range(2))), + ) + @pytest.mark.parametrize( + "is_value_none, expected_event", + ((True, DummyEvent.NONE), (False, DummyEvent.DONE)), + ) + def test_end_block( + self, + selection_key: Union[str, Tuple[str, ...]], + is_value_none: bool, + expected_event: str, + ) -> None: + """Test `end_block` when collection threshold is reached.""" + test_round = DummyCollectNonEmptyUntilThresholdRound( + synchronized_data=self.synchronized_data, + context=MagicMock(), + ) + test_round.selection_key = selection_key + payloads = get_dummy_tx_payloads( + self.participants, is_value_none=is_value_none, is_vote_none=True + ) + for payload in payloads: + test_round.process_payload(payload) + + test_round.collection = {f"test_{i}": payloads[i] for i in range(len(payloads))} + test_round.collection_key = "test" + test_round.done_event = DummyEvent.DONE + test_round.none_event = DummyEvent.NONE + + res = cast(Tuple[BaseSynchronizedData, Enum], test_round.end_block()) + assert res[0].db == self.synchronized_data.db + assert res[1] == expected_event diff --git a/packages/valory/skills/abstract_round_abci/tests/test_behaviours.py b/packages/valory/skills/abstract_round_abci/tests/test_behaviours.py new file mode 100644 index 0000000..1065189 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/test_behaviours.py @@ -0,0 +1,951 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test the behaviours.py module of the skill.""" +# pylint: skip-file + +import platform +from abc import ABC +from calendar import timegm +from datetime import datetime +from pathlib import Path +from typing import Any, Dict, Generator, Optional, Tuple +from unittest import mock +from unittest.mock import MagicMock + +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st + +from packages.valory.skills.abstract_round_abci import PUBLIC_ID +from packages.valory.skills.abstract_round_abci.base import ( + ABCIAppInternalError, + AbciApp, + AbstractRound, + BaseSynchronizedData, + BaseTxPayload, + DegenerateRound, + EventType, + OffenseType, + PendingOffense, + RoundSequence, +) +from packages.valory.skills.abstract_round_abci.behaviour_utils import ( + BaseBehaviour, + DegenerateBehaviour, + TmManager, +) +from packages.valory.skills.abstract_round_abci.behaviours import ( + AbstractRoundBehaviour, + PendingOffencesBehaviour, + _MetaRoundBehaviour, +) +from packages.valory.skills.abstract_round_abci.models import TendermintRecoveryParams +from packages.valory.skills.abstract_round_abci.tests.conftest import profile_name + + +BEHAVIOUR_A_ID = "behaviour_a" +BEHAVIOUR_B_ID = "behaviour_b" +BEHAVIOUR_C_ID = "behaviour_c" +CONCRETE_BACKGROUND_BEHAVIOUR_ID = "background_behaviour" +ROUND_A_ID = "round_a" +ROUND_B_ID = "round_b" +CONCRETE_BACKGROUND_ROUND_ID = "background_round" + + +settings.load_profile(profile_name) + + +def test_skill_public_id() -> None: + """Test skill module public ID""" + + assert PUBLIC_ID.name == Path(__file__).parents[1].name + assert PUBLIC_ID.author == Path(__file__).parents[3].name + + +class RoundA(AbstractRound): + """Round A.""" + + round_id = ROUND_A_ID + payload_class = BaseTxPayload + payload_attribute = "" + synchronized_data_class = BaseSynchronizedData + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, EventType]]: + """End block.""" + + def check_payload(self, payload: BaseTxPayload) -> None: + """Check payload.""" + + def process_payload(self, payload: BaseTxPayload) -> None: + """Process payload.""" + + +class RoundB(AbstractRound): + """Round B.""" + + round_id = ROUND_B_ID + payload_class = BaseTxPayload + payload_attribute = "" + synchronized_data_class = BaseSynchronizedData + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, EventType]]: + """End block.""" + + def check_payload(self, payload: BaseTxPayload) -> None: + """Check payload.""" + + def process_payload(self, payload: BaseTxPayload) -> None: + """Process payload.""" + + +class ConcreteBackgroundRound(AbstractRound): + """Concrete Background Round.""" + + round_id = ROUND_B_ID + payload_class = BaseTxPayload + payload_attribute = "" + synchronized_data_class = BaseSynchronizedData + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, EventType]]: + """End block.""" + + def check_payload(self, payload: BaseTxPayload) -> None: + """Check payload.""" + + def process_payload(self, payload: BaseTxPayload) -> None: + """Process payload.""" + + +class BehaviourA(BaseBehaviour): + """Dummy behaviour.""" + + behaviour_id = BEHAVIOUR_A_ID + matching_round = RoundA + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize behaviour.""" + super().__init__(*args, **kwargs) + self.count = 0 + + def setup(self) -> None: + """Setup behaviour.""" + self.count += 1 + + def async_act(self) -> Generator: + """Dummy act method.""" + yield + + +class BehaviourB(BaseBehaviour): + """Dummy behaviour.""" + + behaviour_id = BEHAVIOUR_B_ID + matching_round = RoundB + + def async_act(self) -> Generator: + """Dummy act method.""" + yield + + +class BehaviourC(BaseBehaviour, ABC): + """Dummy behaviour.""" + + matching_round = MagicMock() + + +def test_auto_behaviour_id() -> None: + """Test that the 'auto_behaviour_id()' method works as expected.""" + + assert BehaviourB.auto_behaviour_id() == BEHAVIOUR_B_ID + assert BehaviourB.behaviour_id == BEHAVIOUR_B_ID + assert BehaviourC.auto_behaviour_id() == "behaviour_c" + assert isinstance(BehaviourC.behaviour_id, property) + + +class ConcreteBackgroundBehaviour(BaseBehaviour): + """Dummy behaviour.""" + + behaviour_id = CONCRETE_BACKGROUND_BEHAVIOUR_ID + matching_round = ConcreteBackgroundRound + + def async_act(self) -> Generator: + """Dummy act method.""" + yield + + +class ConcreteAbciApp(AbciApp): + """Concrete ABCI App.""" + + initial_round_cls = RoundA + transition_function = {RoundA: {MagicMock(): RoundB}} + event_to_timeout: Dict = {} + + +class ConcreteRoundBehaviour(AbstractRoundBehaviour): + """Concrete round behaviour.""" + + abci_app_cls = ConcreteAbciApp + behaviours = {BehaviourA, BehaviourB} # type: ignore + initial_behaviour_cls = BehaviourA + background_behaviours_cls = {ConcreteBackgroundBehaviour} # type: ignore + + +class TestAbstractRoundBehaviour: + """Test 'AbstractRoundBehaviour' class.""" + + def setup(self) -> None: + """Set up the tests.""" + self.round_sequence_mock = MagicMock() + context_mock = MagicMock(params=MagicMock()) + context_mock.state.round_sequence = self.round_sequence_mock + context_mock.state.round_sequence.syncing_up = False + self.round_sequence_mock.block_stall_deadline_expired = False + self.behaviour = ConcreteRoundBehaviour(name="", skill_context=context_mock) + + @pytest.mark.parametrize("use_termination", (True, False)) + def test_setup(self, use_termination: bool) -> None: + """Test 'setup' method.""" + assert self.behaviour.background_behaviours == set() + self.behaviour.context.params.use_termination = use_termination + self.behaviour.setup() + assert self.behaviour.background_behaviours_cls == {ConcreteBackgroundBehaviour} + assert ( + isinstance( + self.behaviour.background_behaviours.pop(), ConcreteBackgroundBehaviour + ) + if use_termination + else self.behaviour.background_behaviours == set() + ) + + def test_teardown(self) -> None: + """Test 'teardown' method.""" + self.behaviour.teardown() + + def test_current_behaviour_return_none(self) -> None: + """Test 'current_behaviour' property return None.""" + assert self.behaviour.current_behaviour is None + + def test_act_current_behaviour_name_is_none(self) -> None: + """Test 'act' with current behaviour None.""" + self.behaviour.tm_manager = self.behaviour.instantiate_behaviour_cls(TmManager) # type: ignore + self.behaviour.current_behaviour = None + with mock.patch.object(self.behaviour, "_process_current_round"): + self.behaviour.act() + + @pytest.mark.parametrize( + "no_round, error", + ( + ( + True, + "Behaviour 'behaviour_without_round' specifies unknown 'unknown' as a matching round. " + "Please make sure that the round is implemented and belongs to the FSM. " + "If 'behaviour_without_round' is a background behaviour, please make sure that it is set correctly, " + "by overriding the corresponding attribute of the chained skill's behaviour.", + ), + (False, "round round_1 is not a matching round of any behaviour"), + ), + ) + def test_check_matching_round_consistency_no_behaviour( + self, no_round: bool, error: str + ) -> None: + """Test classmethod '_check_matching_round_consistency', when no behaviour or round is specified.""" + rounds = [ + MagicMock(**{"auto_round_id.return_value": f"round_{i}"}) for i in range(3) + ] + mock_behaviours = [ + MagicMock(matching_round=round, behaviour_id=f"behaviour_{i}") + for i, round in enumerate(rounds[2:]) + ] + if no_round: + mock_behaviours.append( + MagicMock( + matching_round="unknown", behaviour_id="behaviour_without_round" + ) + ) + + with mock.patch.object( + _MetaRoundBehaviour, "_check_all_required_classattributes_are_set" + ), mock.patch.object( + _MetaRoundBehaviour, "_check_behaviour_id_uniqueness" + ), mock.patch.object( + _MetaRoundBehaviour, "_check_initial_behaviour_in_set_of_behaviours" + ), pytest.raises( + ABCIAppInternalError, + match=error, + ): + + class MyRoundBehaviour(AbstractRoundBehaviour): + abci_app_cls = MagicMock( + get_all_round_classes=lambda _, include_background_rounds: rounds, + final_states={ + rounds[0], + }, + ) + behaviours = mock_behaviours # type: ignore + initial_behaviour_cls = MagicMock() + + def test_check_matching_round_consistency(self) -> None: + """Test classmethod '_check_matching_round_consistency', negative case.""" + rounds = [ + MagicMock(**{"auto_round_id.return_value": f"round_{i}"}) for i in range(3) + ] + mock_behaviours = [ + MagicMock(matching_round=round, behaviour_id=f"behaviour_{i}") + for i, round in enumerate(rounds) + ] + + with mock.patch.object( + _MetaRoundBehaviour, "_check_all_required_classattributes_are_set" + ), mock.patch.object( + _MetaRoundBehaviour, "_check_behaviour_id_uniqueness" + ), mock.patch.object( + _MetaRoundBehaviour, "_check_initial_behaviour_in_set_of_behaviours" + ), pytest.raises( + ABCIAppInternalError, + match="internal error: round round_0 is a final round it shouldn't have any matching behaviours", + ): + + class MyRoundBehaviour(AbstractRoundBehaviour): + abci_app_cls = MagicMock( + get_all_round_classes=lambda _, include_background_rounds: rounds, + final_states={ + rounds[0], + }, + ) + behaviours = mock_behaviours # type: ignore + initial_behaviour_cls = MagicMock() + + @pytest.mark.parametrize("behaviour_cls", (set(), {MagicMock()})) + def test_check_matching_round_consistency_with_bg_rounds( + self, behaviour_cls: set + ) -> None: + """Test classmethod '_check_matching_round_consistency' when a background behaviour class is set.""" + rounds = [ + MagicMock(**{"auto_round_id.return_value": f"round_{i}"}) for i in range(3) + ] + mock_behaviours = ( + [ + MagicMock(matching_round=round_, behaviour_id=f"behaviour_{i}") + for i, round_ in enumerate(rounds[1:]) + ] + if behaviour_cls + else [] + ) + + with mock.patch.object( + _MetaRoundBehaviour, "_check_all_required_classattributes_are_set" + ), mock.patch.object( + _MetaRoundBehaviour, "_check_behaviour_id_uniqueness" + ), mock.patch.object( + _MetaRoundBehaviour, "_check_initial_behaviour_in_set_of_behaviours" + ): + + class MyRoundBehaviour(AbstractRoundBehaviour): + abci_app_cls = MagicMock( + get_all_round_classes=lambda _, include_background_rounds: rounds + if include_background_rounds + else [], + final_states={ + rounds[0], + } + if behaviour_cls + else {}, + ) + behaviours = mock_behaviours # type: ignore + initial_behaviour_cls = MagicMock() + background_behaviours_cls = behaviour_cls + + def test_get_behaviour_id_to_behaviour_mapping_negative(self) -> None: + """Test classmethod '_get_behaviour_id_to_behaviour_mapping', negative case.""" + behaviour_id = "behaviour_id" + behaviour_1 = MagicMock(**{"auto_behaviour_id.return_value": behaviour_id}) + behaviour_2 = MagicMock(**{"auto_behaviour_id.return_value": behaviour_id}) + + with pytest.raises( + ValueError, + match=f"cannot have two behaviours with the same id; got {behaviour_2} and {behaviour_1} both with id '{behaviour_id}'", + ): + with mock.patch.object(_MetaRoundBehaviour, "_check_consistency"): + + class MyRoundBehaviour(AbstractRoundBehaviour): + abci_app_cls = MagicMock + behaviours = [behaviour_1, behaviour_2] # type: ignore + initial_behaviour_cls = MagicMock() + + MyRoundBehaviour(name=MagicMock(), skill_context=MagicMock()) + + def test_get_round_to_behaviour_mapping_two_behaviours_same_round(self) -> None: + """Test classmethod '_get_round_to_behaviour_mapping' when two different behaviours point to the same round.""" + behaviour_id_1 = "behaviour_id_1" + behaviour_id_2 = "behaviour_id_2" + round_cls = RoundA + round_id = round_cls.auto_round_id() + behaviour_1 = MagicMock( + matching_round=round_cls, + **{"auto_behaviour_id.return_value": behaviour_id_1}, + ) + behaviour_2 = MagicMock( + matching_round=round_cls, + **{"auto_behaviour_id.return_value": behaviour_id_2}, + ) + + with pytest.raises( + ValueError, + match=f"the behaviours '{behaviour_2.auto_behaviour_id()}' and '{behaviour_1.auto_behaviour_id()}' point to the same matching round '{round_id}'", + ): + with mock.patch.object(_MetaRoundBehaviour, "_check_consistency"): + + class MyRoundBehaviour(AbstractRoundBehaviour): + abci_app_cls = ConcreteAbciApp + behaviours = [behaviour_1, behaviour_2] # type: ignore + initial_behaviour_cls = behaviour_1 + + MyRoundBehaviour(name=MagicMock(), skill_context=MagicMock()) + + def test_get_round_to_behaviour_mapping_with_final_rounds(self) -> None: + """Test classmethod '_get_round_to_behaviour_mapping' with final rounds.""" + + class FinalRound(DegenerateRound, ABC): + """A final round for testing.""" + + behaviour_id_1 = "behaviour_id_1" + behaviour_1 = MagicMock(behaviour_id=behaviour_id_1, matching_round=RoundA) + + class AbciAppTest(AbciApp): + """Abci App for testing.""" + + initial_round_cls = RoundA + transition_function = {RoundA: {MagicMock(): FinalRound}, FinalRound: {}} + event_to_timeout: Dict = {} + final_states = {FinalRound} + + class MyRoundBehaviour(AbstractRoundBehaviour): + abci_app_cls = AbciAppTest + behaviours = {behaviour_1} + initial_behaviour_cls = behaviour_1 + matching_round = FinalRound + + behaviour = MyRoundBehaviour(name=MagicMock(), skill_context=MagicMock()) + final_behaviour = behaviour._round_to_behaviour[FinalRound] + assert issubclass(final_behaviour, DegenerateBehaviour) + assert ( + final_behaviour.auto_behaviour_id() + == f"degenerate_behaviour_{FinalRound.auto_round_id()}" + ) + + def test_check_behaviour_id_uniqueness_negative(self) -> None: + """Test metaclass method '_check_consistency', negative case.""" + behaviour_id = "behaviour_id" + behaviour_1_cls_name = "Behaviour1" + behaviour_2_cls_name = "Behaviour2" + behaviour_1 = MagicMock( + __name__=behaviour_1_cls_name, + **{"auto_behaviour_id.return_value": behaviour_id}, + ) + behaviour_2 = MagicMock( + __name__=behaviour_2_cls_name, + **{"auto_behaviour_id.return_value": behaviour_id}, + ) + + with pytest.raises( + ABCIAppInternalError, + match=rf"behaviours \['{behaviour_1_cls_name}', '{behaviour_2_cls_name}'\] have the same behaviour id '{behaviour_id}'", + ): + + class MyRoundBehaviour(AbstractRoundBehaviour): + abci_app_cls = MagicMock + behaviours = [behaviour_1, behaviour_2] # type: ignore + initial_behaviour_cls = MagicMock() + + def test_check_consistency_two_behaviours_same_round(self) -> None: + """Test metaclass method '_check_consistency' when two different behaviours point to the same round.""" + behaviour_id_1 = "behaviour_id_1" + behaviour_id_2 = "behaviour_id_2" + round_cls = RoundA + round_id = round_cls.auto_round_id() + behaviour_1 = MagicMock( + matching_round=round_cls, + **{"auto_behaviour_id.return_value": "behaviour_id_1"}, + ) + behaviour_2 = MagicMock( + matching_round=round_cls, + **{"auto_behaviour_id.return_value": "behaviour_id_2"}, + ) + + with pytest.raises( + ABCIAppInternalError, + match=rf"internal error: behaviours \['{behaviour_id_1}', '{behaviour_id_2}'\] have the same matching round '{round_id}'", + ): + + class MyRoundBehaviour(AbstractRoundBehaviour): + abci_app_cls = ConcreteAbciApp + behaviours = [behaviour_1, behaviour_2] # type: ignore + initial_behaviour_cls = behaviour_1 + + def test_check_initial_behaviour_in_set_of_behaviours_negative_case(self) -> None: + """Test classmethod '_check_initial_behaviour_in_set_of_behaviours' when initial behaviour is NOT in the set.""" + behaviour_1 = MagicMock( + matching_round=MagicMock(), + **{"auto_behaviour_id.return_value": "behaviour_id_1"}, + ) + behaviour_2 = MagicMock( + matching_round=MagicMock(), + **{"auto_behaviour_id.return_value": "behaviour_id_2"}, + ) + + with pytest.raises( + ABCIAppInternalError, + match=f"initial behaviour {behaviour_2.auto_behaviour_id()} is not in the set of behaviours", + ): + + class MyRoundBehaviour(AbstractRoundBehaviour): + abci_app_cls = ConcreteAbciApp + behaviours = {behaviour_1} + initial_behaviour_cls = behaviour_2 + + def test_act_no_round_change(self) -> None: + """Test the 'act' method of the behaviour, with no round change.""" + self.round_sequence_mock.current_round = RoundA(MagicMock(), MagicMock()) + self.round_sequence_mock.current_round_height = 0 + + # check that after setup(), current behaviour is initial behaviour + self.behaviour.setup() + assert isinstance(self.behaviour.current_behaviour, BehaviourA) + + with mock.patch.object( + self.behaviour.current_behaviour, "clean_up" + ) as clean_up_mock: + # check that after act(), current behaviour is initial behaviour and `clean_up()` has not been called + self.behaviour.act() + assert isinstance(self.behaviour.current_behaviour, BehaviourA) + clean_up_mock.assert_not_called() + + # check that once the flag done is set, the `clean_up()` has been called + # and `current_behaviour` is set to `None`. + self.behaviour.current_behaviour.set_done() + self.behaviour.act() + assert self.behaviour.current_behaviour is None + clean_up_mock.assert_called_once() + + def test_act_behaviour_setup(self) -> None: + """Test the 'act' method of the FSM behaviour triggers setup() of the behaviour.""" + self.round_sequence_mock.current_round = RoundA(MagicMock(), MagicMock()) + self.round_sequence_mock.current_round_height = 0 + + # check that after setup(), current behaviour is initial behaviour + self.behaviour.setup() + assert isinstance(self.behaviour.current_behaviour, BehaviourA) + + assert self.behaviour.current_behaviour.count == 0 + + with mock.patch.object( + self.behaviour.current_behaviour, "clean_up" + ) as clean_up_mock: + # check that after act() first time, a call to setup has been made + self.behaviour.act() + assert isinstance(self.behaviour.current_behaviour, BehaviourA) + assert self.behaviour.current_behaviour.count == 1 + + # check that after act() second time, no further call to setup + self.behaviour.act() + assert self.behaviour.current_behaviour.count == 1 + + # check that the `clean_up()` has not been called + clean_up_mock.assert_not_called() + + def test_act_with_round_change(self) -> None: + """Test the 'act' method of the behaviour, with round change.""" + self.round_sequence_mock.current_round = RoundA(MagicMock(), MagicMock()) + self.round_sequence_mock.current_round_height = 0 + + # check that after setup(), current behaviour is initial behaviour + self.behaviour.setup() + assert isinstance(self.behaviour.current_behaviour, BehaviourA) + + # check that after act(), current behaviour is initial behaviour + with mock.patch.object( + self.behaviour.current_behaviour, "clean_up" + ) as clean_up_mock: + self.behaviour.act() + assert isinstance(self.behaviour.current_behaviour, BehaviourA) + clean_up_mock.assert_not_called() + + # change the round + self.round_sequence_mock.current_round = RoundB(MagicMock(), MagicMock()) + self.round_sequence_mock.current_round_height = ( + self.round_sequence_mock.current_round_height + 1 + ) + + # check that if the round is changed, the behaviour transition is performed and the clean-up is called + self.behaviour.act() + assert isinstance(self.behaviour.current_behaviour, BehaviourB) + clean_up_mock.assert_called_once() + + def test_act_with_round_change_after_current_behaviour_is_none(self) -> None: + """Test the 'act' method of the behaviour, with round change, after cur behaviour is none.""" + self.behaviour.tm_manager = self.behaviour.instantiate_behaviour_cls(TmManager) # type: ignore + self.round_sequence_mock.current_round = RoundA(MagicMock(), MagicMock()) + self.round_sequence_mock.current_round_height = 0 + + # instantiate behaviour + self.behaviour.current_behaviour = self.behaviour.instantiate_behaviour_cls( + BehaviourA + ) + + with mock.patch.object( + self.behaviour.current_behaviour, "clean_up" + ) as clean_up_mock: + # check that after act(), current behaviour is same behaviour + self.behaviour.act() + assert isinstance(self.behaviour.current_behaviour, BehaviourA) + clean_up_mock.assert_not_called() + + # check that after the behaviour is done, current behaviour is None + self.behaviour.current_behaviour.set_done() + self.behaviour.act() + assert self.behaviour.current_behaviour is None + clean_up_mock.assert_called_once() + + # change the round + self.round_sequence_mock.current_round = RoundB(MagicMock(), MagicMock()) + self.round_sequence_mock.current_round_height = ( + self.round_sequence_mock.current_round_height + 1 + ) + + # check that if the round is changed, the behaviour transition is taken + self.behaviour.act() + assert isinstance(self.behaviour.current_behaviour, BehaviourB) + clean_up_mock.assert_called_once() + + @mock.patch.object( + AbstractRoundBehaviour, + "_process_current_round", + ) + @mock.patch.object( + TmManager, + "tm_communication_unhealthy", + new_callable=mock.PropertyMock, + return_value=False, + ) + @mock.patch.object( + TmManager, + "is_acting", + new_callable=mock.PropertyMock, + return_value=False, + ) + @pytest.mark.parametrize("expected_termination_acting", (True, False)) + def test_termination_behaviour_acting( + self, + _: mock._patch, + __: mock._patch, + ___: mock._patch, + expected_termination_acting: bool, + ) -> None: + """Test if the termination background behaviour is acting only when it should.""" + self.behaviour.context.params.use_termination = expected_termination_acting + self.behaviour.setup() + if expected_termination_acting: + with mock.patch.object( + ConcreteBackgroundBehaviour, + "act_wrapper", + ) as mock_background_act: + self.behaviour.act() + mock_background_act.assert_called() + else: + assert self.behaviour.background_behaviours == set() + + @mock.patch.object( + AbstractRoundBehaviour, + "_process_current_round", + ) + @pytest.mark.parametrize( + ("mock_tm_communication_unhealthy", "mock_is_acting", "expected_fix"), + [ + (True, True, True), + (False, True, True), + (True, False, True), + (False, False, False), + ], + ) + def test_try_fix_call( + self, + _: mock._patch, + mock_tm_communication_unhealthy: bool, + mock_is_acting: bool, + expected_fix: bool, + ) -> None: + """Test that `try_fix` is called when necessary.""" + self.behaviour.tm_manager = self.behaviour.instantiate_behaviour_cls(TmManager) # type: ignore + with mock.patch.object( + TmManager, + "tm_communication_unhealthy", + new_callable=mock.PropertyMock, + return_value=mock_tm_communication_unhealthy, + ), mock.patch.object( + TmManager, + "is_acting", + new_callable=mock.PropertyMock, + return_value=mock_is_acting, + ), mock.patch.object( + TmManager, + "try_fix", + ) as mock_try_fix: + self.behaviour.act() + if expected_fix: + mock_try_fix.assert_called() + else: + mock_try_fix.assert_not_called() + + +def test_meta_round_behaviour_when_instance_not_subclass_of_abstract_round_behaviour() -> ( + None +): + """Test instantiation of meta class when instance not a subclass of abstract round behaviour.""" + + class MyRoundBehaviour(metaclass=_MetaRoundBehaviour): + pass + + +def test_abstract_round_behaviour_instantiation_without_attributes_raises_error() -> ( + None +): + """Test that definition of concrete subclass of AbstractRoundBehavior without attributes raises error.""" + with pytest.raises(ABCIAppInternalError): + + class MyRoundBehaviour(AbstractRoundBehaviour): + pass + + +def test_abstract_round_behaviour_matching_rounds_not_covered() -> None: + """Test that definition of concrete subclass of AbstractRoundBehavior when matching round not covered.""" + with pytest.raises(ABCIAppInternalError): + + class MyRoundBehaviour(AbstractRoundBehaviour): + abci_app_cls = ConcreteAbciApp + behaviours = {BehaviourA} + initial_behaviour_cls = BehaviourA + + +@mock.patch.object( + BaseBehaviour, + "tm_communication_unhealthy", + new_callable=mock.PropertyMock, + return_value=False, +) +def test_self_loops_in_abci_app_reinstantiate_behaviour(_: mock._patch) -> None: + """Test that a self-loop transition in the AbciApp will trigger a transition in the round behaviour.""" + event = MagicMock() + + class AbciAppTest(AbciApp): + initial_round_cls = RoundA + transition_function = {RoundA: {event: RoundA}} + + class RoundBehaviour(AbstractRoundBehaviour): + abci_app_cls = AbciAppTest + behaviours = {BehaviourA} + initial_behaviour_cls = BehaviourA + + round_sequence = RoundSequence(MagicMock(), AbciAppTest) + round_sequence.end_sync() + round_sequence.setup(MagicMock(), MagicMock()) + context_mock = MagicMock() + context_mock.state.round_sequence = round_sequence + behaviour = RoundBehaviour(name="", skill_context=context_mock) + behaviour.setup() + + behaviour_1 = behaviour.current_behaviour + assert isinstance(behaviour_1, BehaviourA) + + round_sequence.abci_app.process_event(event) + + behaviour.act() + behaviour_2 = behaviour.current_behaviour + assert isinstance(behaviour_2, BehaviourA) + assert id(behaviour_1) != id(behaviour_2) + assert behaviour_1 != behaviour_2 + + +class LongRunningBehaviour(BaseBehaviour): + """A behaviour that runs forevever.""" + + behaviour_id = "long_running_behaviour" + matching_round = RoundA + + def async_act(self) -> Generator: + """An act method that simply cycles forever.""" + while True: + # cycle forever + yield + + +def test_reset_should_be_performed_when_tm_unhealthy() -> None: + """Test that hard reset is performed while a behaviour is running, and tendermint communication is unhealthy.""" + event = MagicMock() + + class AbciAppTest(AbciApp): + initial_round_cls = RoundA + transition_function = {RoundA: {event: RoundA}} + + class RoundBehaviour(AbstractRoundBehaviour): + abci_app_cls = AbciAppTest + behaviours = {LongRunningBehaviour} # type: ignore + initial_behaviour_cls = LongRunningBehaviour + + round_sequence = RoundSequence(MagicMock(), AbciAppTest) + round_sequence.end_sync() + round_sequence.setup(MagicMock(), MagicMock()) + context_mock = MagicMock() + context_mock.state.round_sequence = round_sequence + tm_recovery_params = TendermintRecoveryParams( + reset_from_round=RoundA.auto_round_id() + ) + context_mock.state.get_acn_result = MagicMock(return_value=tm_recovery_params) + context_mock.params.ipfs_domain_name = None + behaviour = RoundBehaviour(name="", skill_context=context_mock) + behaviour.setup() + + current_behaviour = behaviour.current_behaviour + assert isinstance(current_behaviour, LongRunningBehaviour) + + # upon entering the behaviour, the tendermint node communication is working well + with mock.patch.object( + RoundSequence, + "block_stall_deadline_expired", + new_callable=mock.PropertyMock, + return_value=False, + ): + behaviour.act() + + def dummy_num_peers( + timeout: Optional[float] = None, + ) -> Generator[None, None, Optional[int]]: + """A dummy method for num_active_peers.""" + # a None response is acceptable here, because tendermint is not healthy + return None + yield + + def dummy_reset_tendermint_with_wait( + on_startup: bool = False, + is_recovery: bool = False, + ) -> Generator[None, None, bool]: + """A dummy method for reset_tendermint_with_wait.""" + # we assume the reset goes through successfully + return True + yield + + # at this point LongRunningBehaviour is running + # while the behaviour is running, the tendermint node + # becomes unhealthy, we expect the node to be reset + with mock.patch.object( + RoundSequence, + "block_stall_deadline_expired", + new_callable=mock.PropertyMock, + return_value=True, + ), mock.patch.object( + BaseBehaviour, + "num_active_peers", + side_effect=dummy_num_peers, + ), mock.patch.object( + BaseBehaviour, + "reset_tendermint_with_wait", + side_effect=dummy_reset_tendermint_with_wait, + ) as mock_reset_tendermint: + behaviour.tm_manager.synchronized_data.max_participants = 3 # type: ignore + assert behaviour.tm_manager is not None + behaviour.tm_manager.gentle_reset_attempted = True + behaviour.act() + mock_reset_tendermint.assert_called() + + +class TestPendingOffencesBehaviour: + """Tests for `PendingOffencesBehaviour`.""" + + behaviour: PendingOffencesBehaviour + + @classmethod + def setup_class(cls) -> None: + """Setup the test class.""" + cls.behaviour = PendingOffencesBehaviour( + name="test", + skill_context=MagicMock(), + ) + + @pytest.mark.skipif( + platform.system() == "Windows", + reason="`timegm` behaves differently on Windows. " + "As a result, the generation of `last_transition_timestamp` is invalid.", + ) + @given( + offence=st.builds( + PendingOffense, + accused_agent_address=st.text(), + round_count=st.integers(min_value=0), + offense_type=st.sampled_from(OffenseType), + last_transition_timestamp=st.floats( + min_value=timegm(datetime(1971, 1, 1).utctimetuple()), + max_value=timegm(datetime(8000, 1, 1).utctimetuple()) - 2000, + ), + time_to_live=st.floats(min_value=1, max_value=2000), + ), + wait_ticks=st.integers(min_value=0, max_value=1000), + expired=st.booleans(), + ) + def test_pending_offences_act( + self, + offence: PendingOffense, + wait_ticks: int, + expired: bool, + ) -> None: + """Test `PendingOffencesBehaviour`.""" + offence_expiration = offence.last_transition_timestamp + offence.time_to_live + offence_expiration += 1 if expired else -1 + self.behaviour.round_sequence.last_round_transition_timestamp = datetime.fromtimestamp( # type: ignore + offence_expiration + ) + + gen = self.behaviour.async_act() + + with mock.patch.object( + self.behaviour, + "send_a2a_transaction", + ) as mock_send_a2a_transaction, mock.patch.object( + self.behaviour, + "wait_until_round_end", + ) as mock_wait_until_round_end, mock.patch.object( + self.behaviour, + "set_done", + ) as mock_set_done: + # while pending offences are empty, the behaviour simply waits + for _ in range(wait_ticks): + next(gen) + + self.behaviour.round_sequence.pending_offences = {offence} + + with pytest.raises(StopIteration): + next(gen) + + check = "assert_not_called" if expired else "assert_called_once" + + for mocked in ( + mock_send_a2a_transaction, + mock_wait_until_round_end, + mock_set_done, + ): + getattr(mocked, check)() diff --git a/packages/valory/skills/abstract_round_abci/tests/test_behaviours_utils.py b/packages/valory/skills/abstract_round_abci/tests/test_behaviours_utils.py new file mode 100644 index 0000000..1991586 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/test_behaviours_utils.py @@ -0,0 +1,2681 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test the behaviours_utils.py module of the skill.""" + +import json +import logging +import platform +import time +from abc import ABC +from datetime import datetime +from enum import Enum +from pathlib import Path +from typing import ( + Any, + Callable, + Dict, + Generator, + List, + Optional, + Tuple, + Type, + Union, + cast, +) +from unittest import mock +from unittest.mock import MagicMock + +import pytest +import pytz # pylint: disable=import-error +from _pytest.logging import LogCaptureFixture + +# pylint: skip-file +from aea.common import JSONLike +from aea.protocols.base import Message +from aea.test_tools.utils import as_context +from aea_test_autonomy.helpers.base import try_send +from hypothesis import given, settings +from hypothesis import strategies as st + +from packages.open_aea.protocols.signing import SigningMessage +from packages.valory.connections.http_client.connection import HttpDialogues +from packages.valory.connections.ipfs.connection import IpfsDialogues +from packages.valory.connections.ipfs.connection import PUBLIC_ID as IPFS_CONNECTION_ID +from packages.valory.protocols.http import HttpMessage +from packages.valory.protocols.ipfs import IpfsMessage +from packages.valory.protocols.ipfs.dialogues import IpfsDialogue +from packages.valory.protocols.ledger_api.custom_types import ( + SignedTransaction, + SignedTransactions, + TransactionDigest, + TransactionDigests, +) +from packages.valory.protocols.ledger_api.message import LedgerApiMessage +from packages.valory.protocols.tendermint import TendermintMessage +from packages.valory.skills.abstract_round_abci import behaviour_utils +from packages.valory.skills.abstract_round_abci.base import ( + AbstractRound, + BaseSynchronizedData, + BaseTxPayload, + DegenerateRound, + LEDGER_API_ADDRESS, + OK_CODE, + Transaction, +) +from packages.valory.skills.abstract_round_abci.behaviour_utils import ( + AsyncBehaviour, + BaseBehaviour, + BaseBehaviourInternalError, + DegenerateBehaviour, + GENESIS_TIME_FMT, + INITIAL_HEIGHT, + IPFSBehaviour, + NON_200_RETURN_CODE_DURING_RESET_THRESHOLD, + RPCResponseStatus, + SendException, + TimeoutException, + TmManager, + _MetaBaseBehaviour, + make_degenerate_behaviour, +) +from packages.valory.skills.abstract_round_abci.io_.ipfs import ( + IPFSInteract, + IPFSInteractionError, +) +from packages.valory.skills.abstract_round_abci.models import ( + SharedState, + TendermintRecoveryParams, +) +from packages.valory.skills.abstract_round_abci.tests.conftest import profile_name + + +_DEFAULT_REQUEST_TIMEOUT = 10.0 +_DEFAULT_REQUEST_RETRY_DELAY = 1.0 +_DEFAULT_TX_MAX_ATTEMPTS = 10 +_DEFAULT_TX_TIMEOUT = 10.0 + +settings.load_profile(profile_name) + + +PACKAGE_DIR = Path(__file__).parent.parent + +# https://github.com/python/cpython/issues/94414 +# https://stackoverflow.com/questions/46133223/maximum-value-of-timestamp +# NOTE: timezone in behaviour_utils._get_reset_params set to UTC +# but hypothesis does not allow passing of the `tzinfo` argument +# hence we add and subtract a day from the actual min / max datetime +MIN_DATETIME_WINDOWS = datetime(1970, 1, 3, 1, 0, 0) +MAX_DATETIME_WINDOWS = datetime(3000, 12, 30, 23, 59, 59) + + +def mock_yield_and_return( + return_value: Any, +) -> Callable[[], Generator[None, None, Any]]: + """Wrapper for a Dummy generator that returns a `bool`.""" + + def yield_and_return(*_: Any, **__: Any) -> Generator[None, None, Any]: + """Dummy generator that returns a `bool`.""" + yield + return return_value + + return yield_and_return + + +def yield_and_return_bool_wrapper( + flag_value: bool, +) -> Callable[[], Generator[None, None, Optional[bool]]]: + """Wrapper for a Dummy generator that returns a `bool`.""" + + def yield_and_return_bool( + **_: bool, + ) -> Generator[None, None, Optional[bool]]: + """Dummy generator that returns a `bool`.""" + yield + return flag_value + + return yield_and_return_bool + + +def yield_and_return_int_wrapper( + value: Optional[int], +) -> Callable[[], Generator[None, None, Optional[int]]]: + """Wrapper for a Dummy generator that returns an `int`.""" + + def yield_and_return_int( + **_: int, + ) -> Generator[None, None, Optional[int]]: + """Dummy generator that returns an `int`.""" + yield + return value + + return yield_and_return_int + + +class AsyncBehaviourTest(AsyncBehaviour, ABC): + """Concrete AsyncBehaviour class for testing purposes.""" + + def async_act_wrapper(self) -> Generator: + """Do async act wrapper. Forwards to 'async_act'.""" + yield from self.async_act() + + def async_act(self) -> Generator: + """Do 'async_act'.""" + yield None + + +def test_async_behaviour_ticks() -> None: + """Test "AsyncBehaviour", only ticks.""" + + class MyAsyncBehaviour(AsyncBehaviourTest): + counter = 0 + + def async_act(self) -> Generator: + self.counter += 1 + yield + self.counter += 1 + yield + self.counter += 1 + + behaviour = MyAsyncBehaviour() + assert behaviour.counter == 0 + behaviour.act() + assert behaviour.counter == 1 + assert behaviour.state == AsyncBehaviour.AsyncState.RUNNING + behaviour.act() + assert behaviour.counter == 2 + assert behaviour.state == AsyncBehaviour.AsyncState.RUNNING + behaviour.act() + assert behaviour.counter == 3 + assert behaviour.state == AsyncBehaviour.AsyncState.READY + + +def test_async_behaviour_wait_for_message() -> None: + """Test 'wait_for_message'.""" + + expected_message = "message" + + class MyAsyncBehaviour(AsyncBehaviourTest): + counter = 0 + message = None + + def async_act(self) -> Generator: + self.counter += 1 + self.message = yield from self.wait_for_message( + lambda message: message == expected_message + ) + self.counter += 1 + + behaviour = MyAsyncBehaviour() + assert behaviour.counter == 0 + behaviour.act() + assert behaviour.counter == 1 + assert behaviour.state == AsyncBehaviour.AsyncState.WAITING_MESSAGE + + # another call to act doesn't change the state (still waiting for message) + behaviour.act() + assert behaviour.counter == 1 + assert behaviour.state == AsyncBehaviour.AsyncState.WAITING_MESSAGE + + # sending a message that does not satisfy the condition won't change state + behaviour.try_send("wrong_message") + behaviour.act() + assert behaviour.counter == 1 + assert behaviour.state == AsyncBehaviour.AsyncState.WAITING_MESSAGE + + # sending a message before it is processed raises an exception + behaviour.try_send("wrong_message") + with pytest.raises(SendException, match="cannot send message"): + behaviour.try_send("wrong_message") + behaviour.act() + + # sending the right message will transition to the next state, + # but only when calling act() + behaviour.try_send(expected_message) + assert behaviour.counter == 1 + assert behaviour.state == AsyncBehaviour.AsyncState.WAITING_MESSAGE + behaviour.act() + assert behaviour.counter == 2 + assert behaviour.message == expected_message + assert behaviour.state == AsyncBehaviour.AsyncState.READY + + +def test_async_behaviour_wait_for_message_raises_timeout_exception() -> None: + """Test 'wait_for_message' when it raises TimeoutException.""" + + with pytest.raises(TimeoutException): + behaviour = AsyncBehaviourTest() + gen = behaviour.wait_for_message(lambda _: False, timeout=0.01) + # trigger function + try_send(gen) + # sleep so to run out the timeout + time.sleep(0.02) + # trigger function and make the exception to raise + try_send(gen) + + +def test_async_behaviour_wait_for_condition() -> None: + """Test 'wait_for_condition' method.""" + + condition = False + + class MyAsyncBehaviour(AsyncBehaviourTest): + counter = 0 + + def async_act(self) -> Generator: + self.counter += 1 + yield from self.wait_for_condition(lambda: condition) + self.counter += 1 + + behaviour = MyAsyncBehaviour() + assert behaviour.counter == 0 + behaviour.act() + assert behaviour.counter == 1 + assert behaviour.state == AsyncBehaviour.AsyncState.RUNNING + + # if condition is false, execution remains at the same point + behaviour.act() + assert behaviour.counter == 1 + assert behaviour.state == AsyncBehaviour.AsyncState.RUNNING + + # if condition is true, execution continues + condition = True + behaviour.act() + assert behaviour.counter == 2 + assert behaviour.state == AsyncBehaviour.AsyncState.READY + + +def test_async_behaviour_wait_for_condition_with_timeout() -> None: + """Test 'wait_for_condition' method with timeout expired.""" + + class MyAsyncBehaviour(AsyncBehaviourTest): + counter = 0 + + def async_act(self) -> Generator: + self.counter += 1 + yield from self.wait_for_condition(lambda: False, timeout=0.05) + + behaviour = MyAsyncBehaviour() + assert behaviour.counter == 0 + behaviour.act() + assert behaviour.counter == 1 + assert behaviour.state == AsyncBehaviour.AsyncState.RUNNING + + # sleep so the timeout expires + time.sleep(0.1) + + # the next call to act raises TimeoutException + with pytest.raises(TimeoutException): + behaviour.act() + + +def test_async_behaviour_sleep() -> None: + """Test 'sleep' method.""" + + timedelta = 0.5 + + class MyAsyncBehaviour(AsyncBehaviourTest): + counter = 0 + first_datetime = None + last_datetime = None + + def async_act_wrapper(self) -> Generator: + yield from self.async_act() + + def async_act(self) -> Generator: + self.first_datetime = datetime.now() + self.counter += 1 + yield from self.sleep(timedelta) + self.counter += 1 + self.last_datetime = datetime.now() + + behaviour = MyAsyncBehaviour() + assert behaviour.counter == 0 + + behaviour.act() + assert behaviour.counter == 1 + assert behaviour.state == AsyncBehaviour.AsyncState.RUNNING + + # calling 'act()' before the sleep interval will keep the behaviour in the same state + behaviour.act() + assert behaviour.counter == 1 + assert behaviour.state == AsyncBehaviour.AsyncState.RUNNING + + # wait the sleep timeout, we give twice the amount of time it takes the behaviour + time.sleep(timedelta * 2) + + assert behaviour.counter == 1 + assert behaviour.state == AsyncBehaviour.AsyncState.RUNNING + behaviour.act() + assert behaviour.counter == 2 + assert behaviour.state == AsyncBehaviour.AsyncState.READY + assert behaviour.last_datetime is not None and behaviour.first_datetime is not None + assert ( + behaviour.last_datetime - behaviour.first_datetime + ).total_seconds() > timedelta + + +def test_async_behaviour_without_yield() -> None: + """Test AsyncBehaviour, async_act without yield/yield from.""" + + class MyAsyncBehaviour(AsyncBehaviourTest): + def async_act_wrapper(self) -> Generator: + return None # type: ignore # need to check design, not sure it's proper case with return None + + behaviour = MyAsyncBehaviour() + behaviour.act() + assert behaviour.state == AsyncBehaviour.AsyncState.READY + + +def test_async_behaviour_raise_stopiteration() -> None: + """Test AsyncBehaviour, async_act raising 'StopIteration'.""" + + class MyAsyncBehaviour(AsyncBehaviourTest): + def async_act_wrapper(self) -> Generator: + raise StopIteration + + behaviour = MyAsyncBehaviour() + behaviour.act() + assert behaviour.state == AsyncBehaviour.AsyncState.READY + + +def test_async_behaviour_stop() -> None: + """Test AsyncBehaviour.stop method.""" + + class MyAsyncBehaviour(AsyncBehaviourTest): + def async_act(self) -> Generator: + yield + + behaviour = MyAsyncBehaviour() + assert behaviour.is_stopped + behaviour.act() + assert not behaviour.is_stopped + behaviour.stop() + assert behaviour.is_stopped + behaviour.stop() + assert behaviour.is_stopped + + +class RoundA(AbstractRound): + """Concrete ABCI round.""" + + round_id = "round_a" + synchronized_data_class = BaseSynchronizedData + payload_class = MagicMock() + payload_attribute = MagicMock() + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: + """Handle end block.""" + + def check_payload(self, payload: BaseTxPayload) -> None: + """Check payload.""" + + def process_payload(self, payload: BaseTxPayload) -> None: + """Process payload.""" + + +class BehaviourATest(BaseBehaviour): + """Concrete BaseBehaviour class.""" + + matching_round: Type[RoundA] = RoundA + + def async_act(self) -> Generator: + """Do the 'async_act'.""" + yield None + + +def _get_status_patch_wrapper( + latest_block_height: int, + app_hash: str, +) -> Callable[[Any, Any], Generator[None, None, MagicMock]]: + """Wrapper for `_get_status` method patch.""" + + def _get_status_patch(*_: Any, **__: Any) -> Generator[None, None, MagicMock]: + """Patch `_get_status` method""" + yield + return MagicMock( + body=json.dumps( + { + "result": { + "sync_info": { + "latest_block_height": latest_block_height, + "latest_app_hash": app_hash, + } + } + } + ).encode() + ) + + return _get_status_patch + + +def _get_status_wrong_patch( + *args: Any, **kwargs: Any +) -> Generator[None, None, MagicMock]: + """Patch `_get_status` method""" + return MagicMock( + body=json.dumps({"result": {"sync_info": {"latest_block_height": -1}}}).encode() + ) + yield + + +def _wait_until_transaction_delivered_patch( + *args: Any, **kwargs: Any +) -> Generator[None, None, Tuple]: + """Patch `_wait_until_transaction_delivered` method""" + return False, HttpMessage( + performative=HttpMessage.Performative.RESPONSE, # type: ignore + body=json.dumps({"tx_result": {"info": "TransactionNotValidError"}}), + ) + yield + + +def dummy_generator_wrapper(return_value: Any = None) -> Callable[[Any], Generator]: + """A wrapper around a dummy generator that yields nothing and returns the given return value.""" + + def dummy_generator(*_: Any, **__: Any) -> Generator[None, None, Any]: + """A dummy generator that yields nothing and returns the given return value.""" + yield + return return_value + + return dummy_generator + + +class TestBaseBehaviour: + """Tests for the 'BaseBehaviour' class.""" + + _DUMMY_CONSENSUS_THRESHOLD = 3 + + def setup(self) -> None: + """Set up the tests.""" + self.context_mock = MagicMock() + self.context_params_mock = MagicMock( + request_timeout=_DEFAULT_REQUEST_TIMEOUT, + request_retry_delay=_DEFAULT_REQUEST_RETRY_DELAY, + tx_timeout=_DEFAULT_TX_TIMEOUT, + max_attempts=_DEFAULT_TX_MAX_ATTEMPTS, + ) + self.context_mock.shared_state = {} + self.context_state_synchronized_data_mock = MagicMock() + self.context_mock.params = self.context_params_mock + self.context_mock.state.synchronized_data = ( + self.context_state_synchronized_data_mock + ) + self.current_round_count = 10 + self.current_reset_index = 10 + self.context_mock.state.synchronized_data.db = MagicMock( + round_count=self.current_round_count, reset_index=self.current_reset_index + ) + self.context_mock.state.round_sequence.current_round_id = "round_a" + self.context_mock.state.round_sequence.syncing_up = False + self.context_mock.state.round_sequence.block_stall_deadline_expired = False + self.context_mock.http_dialogues = HttpDialogues() + self.context_mock.ipfs_dialogues = IpfsDialogues( + connection_id=str(IPFS_CONNECTION_ID) + ) + self.context_mock.outbox = MagicMock(put_message=self.dummy_put_message) + self.context_mock.requests = MagicMock(request_id_to_callback={}) + self.context_mock.handlers.__dict__ = {"http": MagicMock()} + self.behaviour = BehaviourATest(name="", skill_context=self.context_mock) + self.behaviour.context.logger = logging # type: ignore + self.behaviour.params.sleep_time = 0.01 # type: ignore + + def dummy_put_message(self, *args: Any, **kwargs: Any) -> None: + """A dummy implementation of Outbox.put_message""" + return + + def test_behaviour_id(self) -> None: + """Test behaviour_id on instance.""" + assert self.behaviour.behaviour_id == BehaviourATest.auto_behaviour_id() + + @pytest.mark.parametrize( + "ipfs_response, expected_log", + [ + ( + MagicMock( + ipfs_hash="test", performative=IpfsMessage.Performative.IPFS_HASH + ), + "Successfully stored dummy_filename to IPFS with hash: test", + ), + ( + MagicMock( + ipfs_hash="test", performative=IpfsMessage.Performative.ERROR + ), + f"Expected performative {IpfsMessage.Performative.IPFS_HASH} but got {IpfsMessage.Performative.ERROR}.", + ), + ], + ) + def test_send_to_ipfs( + self, + caplog: LogCaptureFixture, + ipfs_response: IpfsMessage, + expected_log: str, + ) -> None: + """Test send_to_ipfs""" + + def dummy_do_ipfs_req( + *args: Any, **kwargs: Any + ) -> Generator[None, None, Optional[IpfsMessage]]: + """A dummy method to be used in mocks.""" + return ipfs_response + yield + + with mock.patch.object( + IPFSBehaviour, + "_build_ipfs_store_file_req", + return_value=(MagicMock(), MagicMock()), + ) as build_req, mock.patch.object( + BaseBehaviour, "_do_ipfs_request", side_effect=dummy_do_ipfs_req + ) as do_req: + generator = self.behaviour.send_to_ipfs("dummy_filename", {}) + try_send(generator) + build_req.assert_called() + do_req.assert_called() + assert expected_log in caplog.text + + def test_ipfs_store_fails(self, caplog: LogCaptureFixture) -> None: + """Test for failure during building store_file_req.""" + expected_logs = "An error occurred while trying to send a file to IPFS:" + with mock.patch.object( + IPFSBehaviour, + "_build_ipfs_store_file_req", + side_effect=IPFSInteractionError, + ), caplog.at_level(logging.ERROR): + generator = self.behaviour.send_to_ipfs("dummy_filename", {}) + try_send(generator) + assert expected_logs in caplog.text + + def test_do_ipfs_request(self) -> None: + """Test _do_ipfs_request""" + message, dialogue = cast( + IpfsDialogues, self.context_mock.ipfs_dialogues + ).create(str(IPFS_CONNECTION_ID), IpfsMessage.Performative.GET_FILES) + message = cast(IpfsMessage, message) + dialogue = cast(IpfsDialogue, dialogue) + + def dummy_wait_for_message( + *args: Any, **kwargs: Any + ) -> Generator[None, None, Message]: + """A dummy implementation of AsyncBehaviour.wait_for_message to be used for mocks.""" + return MagicMock() + yield + + with mock.patch.object( + AsyncBehaviour, "wait_for_message", side_effect=dummy_wait_for_message + ): + gen = self.behaviour._do_ipfs_request( + dialogue, + message, + ) + try_send(gen) + + @pytest.mark.parametrize( + "ipfs_response, expected_log", + [ + ( + MagicMock( + files={"dummy_file_name": "test"}, + performative=IpfsMessage.Performative.FILES, + ), + "Retrieved 1 objects from ipfs.", + ), + ( + MagicMock( + ipfs_hash="test", performative=IpfsMessage.Performative.ERROR + ), + f"Expected performative {IpfsMessage.Performative.FILES} but got {IpfsMessage.Performative.ERROR}.", + ), + ], + ) + def test_get_from_ipfs( + self, + caplog: LogCaptureFixture, + ipfs_response: IpfsMessage, + expected_log: str, + ) -> None: + """Test get_from_ipfs""" + + def dummy_do_ipfs_req( + *args: Any, **kwargs: Any + ) -> Generator[None, None, Optional[IpfsMessage]]: + """A dummy method to be used in mocks.""" + return ipfs_response + yield + + with mock.patch.object( + IPFSBehaviour, + "_build_ipfs_get_file_req", + return_value=(MagicMock(), MagicMock()), + ) as build_req, mock.patch.object( + IPFSBehaviour, + "_deserialize_ipfs_objects", + return_value=MagicMock(), + ), mock.patch.object( + BaseBehaviour, "_do_ipfs_request", side_effect=dummy_do_ipfs_req + ) as do_req: + generator = self.behaviour.get_from_ipfs("dummy_ipfs_hash") + try_send(generator) + build_req.assert_called() + do_req.assert_called() + assert expected_log in caplog.text + + def test_ipfs_get_fails(self, caplog: LogCaptureFixture) -> None: + """Test for failure during building get_files req.""" + expected_logs = "An error occurred while trying to fetch a file from IPFS:" + with mock.patch.object( + IPFSBehaviour, "_build_ipfs_get_file_req", side_effect=IPFSInteractionError + ), caplog.at_level(logging.ERROR): + generator = self.behaviour.get_from_ipfs("dummy_ipfs_hash") + try_send(generator) + assert expected_logs in caplog.text + + def test_params_property(self) -> None: + """Test the 'params' property.""" + assert self.behaviour.params == self.context_params_mock + + def test_synchronized_data_property(self) -> None: + """Test the 'synchronized_data' property.""" + assert ( + self.behaviour.synchronized_data + == self.context_state_synchronized_data_mock + ) + + def test_check_in_round(self) -> None: + """Test 'BaseBehaviour' initialization.""" + expected_round_id = "round" + self.context_mock.state.round_sequence.current_round_id = expected_round_id + assert self.behaviour.check_in_round(expected_round_id) + assert not self.behaviour.check_in_round("wrong round") + + assert not self.behaviour.check_not_in_round(expected_round_id) + assert self.behaviour.check_not_in_round("wrong round") + + func = self.behaviour.is_round_ended(expected_round_id) + assert not func() + + def test_check_in_last_round(self) -> None: + """Test 'BaseBehaviour' initialization.""" + expected_round_id = "round" + self.context_mock.state.round_sequence.last_round_id = expected_round_id + assert self.behaviour.check_in_last_round(expected_round_id) + assert not self.behaviour.check_in_last_round("wrong round") + + assert not self.behaviour.check_not_in_last_round(expected_round_id) + assert self.behaviour.check_not_in_last_round("wrong round") + + assert self.behaviour.check_round_has_finished(expected_round_id) + + def test_check_round_height_has_changed(self) -> None: + """Test 'check_round_height_has_changed'.""" + current_height = 0 + self.context_mock.state.round_sequence.current_round_height = current_height + assert not self.behaviour.check_round_height_has_changed(current_height) + new_height = current_height + 1 + self.context_mock.state.round_sequence.current_round_height = new_height + assert self.behaviour.check_round_height_has_changed(current_height) + assert not self.behaviour.check_round_height_has_changed(new_height) + + def test_wait_until_round_end_negative_last_round_or_matching_round(self) -> None: + """Test 'wait_until_round_end' method, negative case (not in matching nor last round).""" + self.behaviour.context.state.round_sequence.current_round_id = ( + "current_round_id" + ) + self.behaviour.context.state.round_sequence.last_round_id = "last_round_id" + self.behaviour.matching_round.round_id = "matching_round" + generator = self.behaviour.wait_until_round_end() + with pytest.raises( + ValueError, + match=r"Should be in matching round \(matching_round\) or last round \(last_round_id\), actual round current_round_id!", + ): + generator.send(None) + + @mock.patch.object(BaseBehaviour, "wait_for_condition") + @mock.patch.object(BaseBehaviour, "check_not_in_round", return_value=False) + @mock.patch.object(BaseBehaviour, "check_not_in_last_round", return_value=False) + def test_wait_until_round_end_positive(self, *_: Any) -> None: + """Test 'wait_until_round_end' method, positive case.""" + gen = self.behaviour.wait_until_round_end() + try_send(gen) + + def test_wait_from_last_timestamp(self) -> None: + """Test 'wait_from_last_timestamp'.""" + timeout = 1.0 + last_timestamp = datetime.now() + self.behaviour.context.state.round_sequence.abci_app.last_timestamp = ( + last_timestamp + ) + gen = self.behaviour.wait_from_last_timestamp(timeout) + # trigger first execution + try_send(gen) + # at the time this line is executed, the generator is not empty + # as the timeout has not run out yet + try_send(gen) + # sleep enough time to make the timeout to run out + time.sleep(timeout) + # the next iteration of the generator raises StopIteration + # because its execution terminates + with pytest.raises(StopIteration): + gen.send(MagicMock()) + + def test_wait_from_last_timestamp_negative(self) -> None: + """Test 'wait_from_last_timestamp'.""" + timeout = -1.0 + last_timestamp = datetime.now() + self.behaviour.context.state.round_sequence.abci_app.last_timestamp = ( + last_timestamp + ) + with pytest.raises(ValueError): + gen = self.behaviour.wait_from_last_timestamp(timeout) + # trigger first execution + try_send(gen) + + def test_set_done(self) -> None: + """Test 'set_done' method.""" + assert not self.behaviour.is_done() + self.behaviour.set_done() + assert self.behaviour.is_done() + + @mock.patch.object(BaseBehaviour, "_send_transaction") + def test_send_a2a_transaction_positive(self, *_: Any) -> None: + """Test 'send_a2a_transaction' method, positive case.""" + gen = self.behaviour.send_a2a_transaction(MagicMock()) + try_send(gen) + + def test_async_act_wrapper_agent_sync_mode( + self, + ) -> None: + """Test 'async_act_wrapper' in sync mode.""" + self.behaviour.context.state.round_sequence.syncing_up = True + self.behaviour.context.state.round_sequence.height = 0 + self.behaviour.matching_round = MagicMock() + + with mock.patch.object(logging, "info") as log_mock, mock.patch.object( + BaseBehaviour, + "_get_status", + _get_status_patch_wrapper(0, "test"), + ): + gen = self.behaviour.async_act_wrapper() + for __ in range(3): + try_send(gen) + log_mock.assert_called_with( + "local height == remote == 0; Synchronization complete." + ) + + @mock.patch.object(BaseBehaviour, "_get_status", _get_status_wrong_patch) + def test_async_act_wrapper_agent_sync_mode_where_height_dont_match(self) -> None: + """Test 'async_act_wrapper' in sync mode.""" + self.behaviour.context.state.round_sequence.syncing_up = True + self.behaviour.context.state.round_sequence.height = 0 + self.behaviour.context.params.tendermint_check_sleep_delay = 3 + self.behaviour.matching_round = MagicMock() + + gen = self.behaviour.async_act_wrapper() + try_send(gen) + + @pytest.mark.parametrize("exception_cls", [StopIteration]) + def test_async_act_wrapper_exception(self, exception_cls: Exception) -> None: + """Test 'async_act_wrapper'.""" + with mock.patch.object(self.behaviour, "async_act", side_effect=exception_cls): + with mock.patch.object(self.behaviour, "clean_up") as clean_up_mock: + gen = self.behaviour.async_act_wrapper() + try_send(gen) + try_send(gen) + clean_up_mock.assert_called() + + def test_get_request_nonce_from_dialogue(self) -> None: + """Test '_get_request_nonce_from_dialogue' helper method.""" + dialogue_mock = MagicMock() + expected_value = "dialogue_reference" + dialogue_mock.dialogue_label.dialogue_reference = (expected_value, None) + result = BaseBehaviour._get_request_nonce_from_dialogue(dialogue_mock) + assert result == expected_value + + @mock.patch.object(BaseBehaviour, "_send_signing_request") + @mock.patch.object(Transaction, "encode", return_value=MagicMock()) + @mock.patch.object( + BaseBehaviour, + "_build_http_request_message", + return_value=(MagicMock(), MagicMock()), + ) + @mock.patch.object(BaseBehaviour, "_check_http_return_code_200", return_value=False) + def test_send_transaction_stop_condition(self, *_: Any) -> None: + """Test '_send_transaction' method's `stop_condition` as provided by `send_a2a_transaction`.""" + request_retry_delay = 0.01 + # set the current round's id so that it does not meet the requirements for a `stop_condition` + self.behaviour.context.state.round_sequence.current_round_id = ( + self.behaviour.matching_round.round_id + ) = "round_a" + # assert that everything is pre-set correctly + assert ( + self.behaviour.context.state.round_sequence.current_round_id + == self.behaviour.matching_round.auto_round_id() + == "round_a" + ) + + # create the exact same stop condition that we create in the `send_a2a_transaction` method + stop_condition = self.behaviour.is_round_ended( + self.behaviour.matching_round.auto_round_id() + ) + gen = self.behaviour._send_transaction( + MagicMock(), + request_retry_delay=request_retry_delay, + stop_condition=stop_condition, + ) + # assert that the stop condition does not apply yet + assert not stop_condition() + # trigger the generator function so that we enter the `stop_condition` loop + try_send(gen) + + # set the current round's id so that it meets the requirements for a `stop_condition` + self.behaviour.context.state.round_sequence.current_round_id = "test" + + # assert that everything was set as expected + assert ( + self.behaviour.context.state.round_sequence.current_round_id + != self.behaviour.matching_round.auto_round_id() + and self.behaviour.context.state.round_sequence.current_round_id == "test" + ) + # assert that the stop condition now applies + assert stop_condition() + + # test with a non-200 response in order to cause the execution to re-enter the while `stop_condition` + # we expect that the second time we will not enter, since we have caused the `stop_condition` to be `True` + with mock.patch.object( + self.behaviour.context.logger, "debug" + ) as mock_debug, mock.patch.object( + self.behaviour.context.logger, "error" + ) as mock_error: + # send message to 'wait_for_message' + try_send(gen, obj=MagicMock(status_code=200)) + # send message to '_submit_tx' + response = MagicMock(body='{"result": {"hash": "", "code": 0}}') + try_send(gen, obj=response) + mock_error.assert_called_with( + f"Received return code != 200 with response {response} with body {str(response.body)}. " + f"Retrying in {request_retry_delay} seconds..." + ) + time.sleep(request_retry_delay) + try_send(gen) + # assert that the stop condition is now `True` and we reach at the end of the method + mock_debug.assert_called_with( + "Stop condition is true, no more attempts to send the transaction." + ) + + def test_send_transaction_positive_false_condition(self) -> None: + """Test '_send_transaction', positive case (false condition)""" + with mock.patch.object(self.behaviour.context.logger, "debug") as mock_debug: + try_send( + self.behaviour._send_transaction( + MagicMock(), stop_condition=lambda: True + ) + ) + mock_debug.assert_called_with( + "Stop condition is true, no more attempts to send the transaction." + ) + + @mock.patch.object(BaseBehaviour, "_send_signing_request") + @mock.patch.object(Transaction, "encode", return_value=MagicMock()) + @mock.patch.object( + BaseBehaviour, + "_build_http_request_message", + return_value=(MagicMock(), MagicMock()), + ) + @mock.patch.object(BaseBehaviour, "_check_http_return_code_200", return_value=True) + def test_send_transaction_positive(self, *_: Any) -> None: + """Test '_send_transaction', positive case.""" + m = MagicMock(status_code=200) + gen = self.behaviour._send_transaction(m) + # trigger generator function + try_send(gen, obj=None) + # send message to 'wait_for_message' + try_send(gen, obj=m) + # send message to '_submit_tx' + try_send(gen, obj=MagicMock(body='{"result": {"hash": "", "code": 0}}')) + # send message to '_wait_until_transaction_delivered' + success_response = MagicMock( + status_code=200, body='{"result": {"tx_result": {"code": 0}}}' + ) + try_send(gen, obj=success_response) + + @mock.patch.object(BaseBehaviour, "_send_signing_request") + @mock.patch.object(Transaction, "encode", return_value=MagicMock()) + @mock.patch.object( + BaseBehaviour, + "_build_http_request_message", + return_value=(MagicMock(), MagicMock()), + ) + @mock.patch.object(BaseBehaviour, "_check_http_return_code_200", return_value=True) + @mock.patch.object( + BaseBehaviour, + "_wait_until_transaction_delivered", + new=_wait_until_transaction_delivered_patch, + ) + def test_send_transaction_invalid_transaction(self, *_: Any) -> None: + """Test '_send_transaction', positive case.""" + m = MagicMock(status_code=200) + gen = self.behaviour._send_transaction(m) + try_send(gen, obj=None) + try_send(gen, obj=m) + try_send(gen, obj=MagicMock(body='{"result": {"hash": "", "code": 0}}')) + success_response = MagicMock( + status_code=200, body='{"result": {"tx_result": {"code": 0}}}' + ) + try_send(gen, obj=success_response) + + @mock.patch.object(BaseBehaviour, "_send_signing_request") + @mock.patch.object(BaseBehaviour, "_is_invalid_transaction", return_value=False) + @mock.patch.object(BaseBehaviour, "_tx_not_found", return_value=True) + @mock.patch.object(Transaction, "encode", return_value=MagicMock()) + @mock.patch.object( + BaseBehaviour, + "_build_http_request_message", + return_value=(MagicMock(), MagicMock()), + ) + @mock.patch.object(BaseBehaviour, "_check_http_return_code_200", return_value=True) + @mock.patch.object( + BaseBehaviour, + "_wait_until_transaction_delivered", + new=_wait_until_transaction_delivered_patch, + ) + def test_send_transaction_valid_transaction(self, *_: Any) -> None: + """Test '_send_transaction', positive case.""" + m = MagicMock(status_code=200) + gen = self.behaviour._send_transaction(m) + try_send(gen, obj=None) + try_send(gen, obj=m) + try_send(gen, obj=MagicMock(body='{"result": {"hash": "", "code": 0}}')) + success_response = MagicMock( + status_code=200, body='{"result": {"tx_result": {"code": 0}}}' + ) + try_send(gen, obj=success_response) + + def test_tx_not_found(self, *_: Any) -> None: + """Test _tx_not_found""" + res = MagicMock( + body='{"error": {"code": "dummy_code", "message": "dummy_message", "data": "dummy_data"}}' + ) + self.behaviour._tx_not_found(tx_hash="tx_hash", res=res) + + @mock.patch.object(BaseBehaviour, "_send_signing_request") + def test_send_transaction_signing_error(self, *_: Any) -> None: + """Test '_send_transaction', signing error.""" + m = MagicMock(performative=SigningMessage.Performative.ERROR) + gen = self.behaviour._send_transaction(m) + # trigger generator function + try_send(gen, obj=None) + with pytest.raises(RuntimeError): + try_send(gen, obj=m) + + @mock.patch.object(BaseBehaviour, "_send_signing_request") + @mock.patch.object(Transaction, "encode", return_value=MagicMock()) + @mock.patch.object( + BaseBehaviour, + "_build_http_request_message", + return_value=(MagicMock(), MagicMock()), + ) + def test_send_transaction_timeout_exception_submit_tx(self, *_: Any) -> None: + """Test '_send_transaction', timeout exception.""" + timeout = 0.05 + delay = 0.1 + m = MagicMock() + with mock.patch.object( + self.behaviour.context.logger, "warning" + ) as mock_warning: + gen = self.behaviour._send_transaction( + m, request_timeout=timeout, request_retry_delay=delay + ) + # trigger generator function + try_send(gen, obj=None) + try_send(gen, obj=m) + time.sleep(timeout) + try_send(gen, obj=m) + time.sleep(delay) + mock_warning.assert_called_with( + f"Timeout expired for submit tx. Retrying in {delay} seconds..." + ) + try_send(gen, obj=None) + + @mock.patch.object(BaseBehaviour, "_send_signing_request") + @mock.patch.object(Transaction, "encode", return_value=MagicMock()) + @mock.patch.object( + BaseBehaviour, + "_build_http_request_message", + return_value=(MagicMock(), MagicMock()), + ) + @mock.patch.object(BaseBehaviour, "_check_http_return_code_200", return_value=True) + def test_send_transaction_timeout_exception_wait_until_transaction_delivered( + self, *_: Any + ) -> None: + """Test '_send_transaction', timeout exception.""" + timeout = 0.05 + delay = 0.1 + m = MagicMock() + with mock.patch.object( + self.behaviour.context.logger, "warning" + ) as mock_warning: + gen = self.behaviour._send_transaction( + m, request_retry_delay=delay, tx_timeout=timeout + ) + # trigger generator function + try_send(gen, obj=None) + # send message to 'wait_for_message' + try_send(gen, obj=m) + # send message to '_submit_tx' + try_send(gen, obj=MagicMock(body='{"result": {"hash": "", "code": 0}}')) + # send message to '_wait_until_transaction_delivered' + time.sleep(timeout) + try_send(gen, obj=m) + + mock_warning.assert_called_with( + f"Timeout expired for wait until transaction delivered. Retrying in {delay} seconds..." + ) + + @mock.patch.object(BaseBehaviour, "_send_signing_request") + @mock.patch.object(Transaction, "encode", return_value=MagicMock()) + @mock.patch.object( + BaseBehaviour, + "_build_http_request_message", + return_value=(MagicMock(), MagicMock()), + ) + @mock.patch.object(BaseBehaviour, "_check_http_return_code_200", return_value=True) + def test_send_transaction_transaction_not_delivered(self, *_: Any) -> None: + """Test '_send_transaction', timeout exception.""" + timeout = 0.05 + delay = 0.1 + m = MagicMock() + with mock.patch.object( + self.behaviour.context.logger, "warning" + ) as mock_warning: + gen = self.behaviour._send_transaction( + m, request_retry_delay=delay, tx_timeout=timeout, max_attempts=0 + ) + # trigger generator function + try_send(gen, obj=None) + # send message to 'wait_for_message' + try_send(gen, obj=m) + # send message to '_submit_tx' + try_send(gen, obj=MagicMock(body='{"result": {"hash": "", "code": 0}}')) + # send message to '_wait_until_transaction_delivered' + time.sleep(timeout) + try_send(gen, obj=m) + + mock_warning.assert_called_with( + "Tx sent but not delivered. Response = None" + ) + + @mock.patch.object(BaseBehaviour, "_send_signing_request") + @mock.patch.object(Transaction, "encode", return_value=MagicMock()) + @mock.patch.object( + BaseBehaviour, + "_build_http_request_message", + return_value=(MagicMock(), MagicMock()), + ) + @mock.patch.object(BaseBehaviour, "_check_http_return_code_200", return_value=True) + def test_send_transaction_wrong_ok_code(self, *_: Any) -> None: + """Test '_send_transaction', positive case.""" + m = MagicMock(status_code=200) + gen = self.behaviour._send_transaction(m) + + with mock.patch.object(self.behaviour.context.logger, "error") as mock_error: + # trigger generator function + try_send(gen, obj=None) + # send message to 'wait_for_message' + try_send(gen, obj=m) + # send message to '_submit_tx' + try_send(gen, obj=MagicMock(body='{"result": {"hash": "", "code": -1}}')) + # send message to '_wait_until_transaction_delivered' + success_response = MagicMock( + status_code=200, body='{"result": {"tx_result": {"code": 0}}}' + ) + try_send(gen, obj=success_response) + + mock_error.assert_called_with( + "Received tendermint code != 0. Retrying in 1.0 seconds..." + ) + + @mock.patch.object(BaseBehaviour, "_send_signing_request") + @mock.patch.object(Transaction, "encode", return_value=MagicMock()) + @mock.patch.object( + BaseBehaviour, + "_build_http_request_message", + return_value=(MagicMock(), MagicMock()), + ) + @mock.patch.object( + BaseBehaviour, + "_check_http_return_code_200", + return_value=True, + ) + @mock.patch("json.loads", return_value={"result": {"hash": "", "code": OK_CODE}}) + def test_send_transaction_wait_delivery_timeout_exception(self, *_: Any) -> None: + """Test '_send_transaction', timeout exception on tx delivery.""" + timeout = 0.05 + delay = 0.1 + m = MagicMock() + with mock.patch.object( + self.behaviour.context.logger, "warning" + ) as mock_warning: + gen = self.behaviour._send_transaction( + m, + request_timeout=timeout, + request_retry_delay=delay, + tx_timeout=timeout, + ) + # trigger generator function + try_send(gen, obj=None) + try_send(gen, obj=m) + try_send(gen, obj=m) + time.sleep(timeout) + try_send(gen, obj=m) + mock_warning.assert_called_with( + f"Timeout expired for wait until transaction delivered. Retrying in {delay} seconds..." + ) + time.sleep(delay) + try_send(gen, obj=m) + + @pytest.mark.parametrize("resetting", (True, False)) + @pytest.mark.parametrize( + "non_200_count", + ( + 0, + NON_200_RETURN_CODE_DURING_RESET_THRESHOLD, + NON_200_RETURN_CODE_DURING_RESET_THRESHOLD + 1, + ), + ) + @mock.patch.object(BaseBehaviour, "_send_signing_request") + @mock.patch.object(Transaction, "encode", return_value=MagicMock()) + @mock.patch.object( + BaseBehaviour, + "_build_http_request_message", + return_value=(MagicMock(), MagicMock()), + ) + @mock.patch("json.loads") + def test_send_transaction_error_status_code( + self, _: Any, __: Any, ___: Any, ____: Any, resetting: bool, non_200_count: int + ) -> None: + """Test '_send_transaction', error status code.""" + delay = 0.1 + self.behaviour._non_200_return_code_count = non_200_count + m = MagicMock() + with mock.patch.object(self.behaviour.context.logger, "error") as mock_error: + gen = self.behaviour._send_transaction( + m, resetting, request_retry_delay=delay + ) + # trigger generator function + try_send(gen, obj=None) + try_send(gen, obj=m) + # send message to '_submit_tx' + res = MagicMock(body="{'test': 'test'}") + try_send(gen, obj=res) + if ( + resetting + and non_200_count <= NON_200_RETURN_CODE_DURING_RESET_THRESHOLD + ): + mock_error.assert_not_called() + else: + mock_error.assert_called_with( + f"Received return code != 200 with response {res} with body {str(res.body)}. " + f"Retrying in {delay} seconds..." + ) + time.sleep(delay) + try_send(gen, obj=None) + + @mock.patch.object(BaseBehaviour, "_get_request_nonce_from_dialogue") + @mock.patch.object(behaviour_utils, "RawMessage") + @mock.patch.object(behaviour_utils, "Terms") + def test_send_signing_request(self, *_: Any) -> None: + """Test '_send_signing_request'.""" + with mock.patch.object( + self.behaviour.context.signing_dialogues, + "create", + return_value=(MagicMock(), MagicMock()), + ): + self.behaviour._send_signing_request(b"") + + @given(st.binary()) + def test_fuzz_send_signing_request(self, input_bytes: bytes) -> None: + """Fuzz '_send_signing_request'. + + Mock context manager decorators don't work here. + + :param input_bytes: fuzz input + """ + with mock.patch.object( + self.behaviour.context.signing_dialogues, + "create", + return_value=(MagicMock(), MagicMock()), + ): + with mock.patch.object(behaviour_utils, "RawMessage"): + with mock.patch.object(behaviour_utils, "Terms"): + self.behaviour._send_signing_request(input_bytes) + + @mock.patch.object(BaseBehaviour, "_get_request_nonce_from_dialogue") + @mock.patch.object(behaviour_utils, "RawMessage") + @mock.patch.object(behaviour_utils, "Terms") + def test_send_transaction_signing_request(self, *_: Any) -> None: + """Test '_send_signing_request'.""" + with mock.patch.object( + self.behaviour.context.signing_dialogues, + "create", + return_value=(MagicMock(), MagicMock()), + ): + self.behaviour._send_transaction_signing_request(MagicMock(), MagicMock()) + + @pytest.mark.parametrize( + "use_flashbots, target_block_numbers, expected_kwargs", + ( + ( + True, + None, + dict( + counterparty=LEDGER_API_ADDRESS, + performative=LedgerApiMessage.Performative.SEND_SIGNED_TRANSACTIONS, + signed_transactions=SignedTransactions( + ledger_id="ethereum_flashbots", + signed_transactions=[{"test_tx": "test_tx"}], + ), + kwargs=LedgerApiMessage.Kwargs( + { + "chain_id": None, + "raise_on_failed_simulation": False, + "use_all_builders": True, + } + ), + ), + ), + ( + True, + [1, 2, 3], + dict( + counterparty=LEDGER_API_ADDRESS, + performative=LedgerApiMessage.Performative.SEND_SIGNED_TRANSACTIONS, + signed_transactions=SignedTransactions( + ledger_id="ethereum_flashbots", + signed_transactions=[{"test_tx": "test_tx"}], + ), + kwargs=LedgerApiMessage.Kwargs( + { + "chain_id": None, + "raise_on_failed_simulation": False, + "use_all_builders": True, + "target_block_numbers": [1, 2, 3], + } + ), + ), + ), + ( + False, + None, + dict( + counterparty=LEDGER_API_ADDRESS, + performative=LedgerApiMessage.Performative.SEND_SIGNED_TRANSACTION, + signed_transaction=SignedTransaction( + ledger_id="ethereum", body={"test_tx": "test_tx"} + ), + ), + ), + ), + ) + def test_send_transaction_request( + self, + use_flashbots: bool, + target_block_numbers: Optional[List[int]], + expected_kwargs: Any, + ) -> None: + """Test '_send_transaction_request'.""" + with mock.patch.object( + self.behaviour.context.ledger_api_dialogues, + "create", + return_value=(MagicMock(), MagicMock()), + ) as create_mock: + self.behaviour._send_transaction_request( + MagicMock( + signed_transaction=SignedTransaction( + ledger_id="ethereum", body={"test_tx": "test_tx"} + ) + ), + use_flashbots, + target_block_numbers, + ) + create_mock.assert_called_once() + # not using `create_mock.call_args.kwargs` because it is not compatible with Python 3.7 + actual_kwargs = create_mock.call_args[1] + assert actual_kwargs == expected_kwargs + + def test_send_transaction_receipt_request(self) -> None: + """Test '_send_transaction_receipt_request'.""" + with mock.patch.object( + self.behaviour.context.ledger_api_dialogues, + "create", + return_value=(MagicMock(), MagicMock()), + ): + self.behaviour.context.default_ledger_id = "default_ledger_id" + self.behaviour._send_transaction_receipt_request("digest") + + def test_build_http_request_message(self, *_: Any) -> None: + """Test '_build_http_request_message'.""" + with mock.patch.object( + self.behaviour.context.http_dialogues, + "create", + return_value=(MagicMock(), MagicMock()), + ): + self.behaviour._build_http_request_message( + "", + "", + parameters={"foo": "bar"}, + headers={"foo": "foo_val", "bar": "bar_val"}, + ) + + @mock.patch.object(Transaction, "encode", return_value=MagicMock()) + @mock.patch.object( + BaseBehaviour, + "_build_http_request_message", + return_value=(MagicMock(), MagicMock()), + ) + @mock.patch.object(BaseBehaviour, "_check_http_return_code_200", return_value=True) + @mock.patch.object(BaseBehaviour, "sleep") + @mock.patch("json.loads") + def test_wait_until_transaction_delivered(self, *_: Any) -> None: + """Test '_wait_until_transaction_delivered' method.""" + gen = self.behaviour._wait_until_transaction_delivered(MagicMock()) + # trigger generator function + try_send(gen, obj=None) + + # first check attempt fails + failure_response = MagicMock(status_code=500) + try_send(gen, failure_response) + + # second check attempt succeeds + success_response = MagicMock( + status_code=200, body='{"result": {"tx_result": {"code": 0}}}' + ) + try_send(gen, success_response) + + @mock.patch.object(Transaction, "encode", return_value=MagicMock()) + @mock.patch.object( + BaseBehaviour, + "_build_http_request_message", + return_value=(MagicMock(), MagicMock()), + ) + @mock.patch.object(BaseBehaviour, "_check_http_return_code_200", return_value=True) + @mock.patch.object(BaseBehaviour, "sleep") + @mock.patch("json.loads") + def test_wait_until_transaction_delivered_failed(self, *_: Any) -> None: + """Test '_wait_until_transaction_delivered' method.""" + gen = self.behaviour._wait_until_transaction_delivered( + MagicMock(), max_attempts=0 + ) + # trigger generator function + try_send(gen, obj=None) + + # first check attempt fails + failure_response = MagicMock(status_code=500) + try_send(gen, failure_response) + + # second check attempt succeeds + success_response = MagicMock( + status_code=200, body='{"result": {"tx_result": {"code": -1}}}' + ) + try_send(gen, success_response) + + @pytest.mark.skipif( + platform.system() == "Windows", + reason="https://github.com/valory-xyz/open-autonomy/issues/1477", + ) + def test_wait_until_transaction_delivered_raises_timeout(self, *_: Any) -> None: + """Test '_wait_until_transaction_delivered' method.""" + gen = self.behaviour._wait_until_transaction_delivered(MagicMock(), timeout=0.0) + with pytest.raises(TimeoutException): + # trigger generator function + try_send(gen, obj=None) + + @mock.patch.object(behaviour_utils, "Terms") + def test_get_default_terms(self, *_: Any) -> None: + """Test '_get_default_terms'.""" + self.behaviour._get_default_terms() + + @mock.patch.object(BaseBehaviour, "_send_transaction_signing_request") + @mock.patch.object(BaseBehaviour, "_send_transaction_request") + @mock.patch.object(BaseBehaviour, "_send_transaction_receipt_request") + @mock.patch.object(behaviour_utils, "Terms") + @pytest.mark.parametrize( + "ledger_message, expected_hash, expected_response_status", + ( + ( + LedgerApiMessage( + cast( + LedgerApiMessage.Performative, + LedgerApiMessage.Performative.TRANSACTION_DIGEST, + ), + ("", ""), + transaction_digest=TransactionDigest("ledger_id", body="test"), + ), + "test", + RPCResponseStatus.SUCCESS, + ), + ( + LedgerApiMessage( + cast( + LedgerApiMessage.Performative, + LedgerApiMessage.Performative.TRANSACTION_DIGESTS, + ), + ("", ""), + # Only the first hash will be considered + # because we do not support sending multiple messages and receiving multiple tx hashes yet + transaction_digests=TransactionDigests( + "ledger_id", + transaction_digests=["test", "will_not_be_considered"], + ), + ), + "test", + RPCResponseStatus.SUCCESS, + ), + ), + ) + def test_send_raw_transaction( + self, + _send_transaction_signing_request: Any, + _send_transaction_request: Any, + _send_transaction_receipt_request: Any, + _terms: Any, + ledger_message: LedgerApiMessage, + expected_hash: str, + expected_response_status: RPCResponseStatus, + ) -> None: + """Test 'send_raw_transaction'.""" + m = MagicMock() + gen = self.behaviour.send_raw_transaction(m) + # trigger generator function + gen.send(None) + gen.send( + SigningMessage( + cast( + SigningMessage.Performative, + SigningMessage.Performative.SIGNED_TRANSACTION, + ), + ("", ""), + signed_transaction=SignedTransaction( + "ledger_id", body={"hash": expected_hash} + ), + ) + ) + try: + gen.send(ledger_message) + raise ValueError("Generator was expected to have reached its end!") + except StopIteration as e: + tx_hash, status = e.value + + assert tx_hash == expected_hash + assert status == expected_response_status + + @mock.patch.object(BaseBehaviour, "_send_transaction_signing_request") + @mock.patch.object(BaseBehaviour, "_send_transaction_request") + @mock.patch.object(BaseBehaviour, "_send_transaction_receipt_request") + @mock.patch.object(behaviour_utils, "Terms") + def test_send_raw_transaction_with_wrong_signing_performative( + self, *_: Any + ) -> None: + """Test 'send_raw_transaction'.""" + m = MagicMock() + gen = self.behaviour.send_raw_transaction(m) + # trigger generator function + gen.send(None) + try: + gen.send(MagicMock(performative=SigningMessage.Performative.ERROR)) + raise ValueError("Generator was expected to have reached its end!") + except StopIteration as e: + tx_hash, status = e.value + + assert tx_hash is None + assert status == RPCResponseStatus.UNCLASSIFIED_ERROR + + @pytest.mark.parametrize( + "message, expected_rpc_status", + ( + ("Simulation failed for bundle", RPCResponseStatus.SIMULATION_FAILED), + ("replacement transaction underpriced", RPCResponseStatus.UNDERPRICED), + ("nonce too low", RPCResponseStatus.INCORRECT_NONCE), + ("insufficient funds", RPCResponseStatus.INSUFFICIENT_FUNDS), + ("already known", RPCResponseStatus.ALREADY_KNOWN), + ("test", RPCResponseStatus.UNCLASSIFIED_ERROR), + ), + ) + @mock.patch.object(BaseBehaviour, "_send_transaction_signing_request") + @mock.patch.object(BaseBehaviour, "_send_transaction_request") + @mock.patch.object(BaseBehaviour, "_send_transaction_receipt_request") + @mock.patch.object(behaviour_utils, "Terms") + def test_send_raw_transaction_errors( + self, + _: Any, + __: Any, + ___: Any, + ____: Any, + message: str, + expected_rpc_status: RPCResponseStatus, + ) -> None: + """Test 'send_raw_transaction'.""" + m = MagicMock() + gen = self.behaviour.send_raw_transaction(m) + # trigger generator function + gen.send(None) + gen.send( + SigningMessage( + cast( + SigningMessage.Performative, + SigningMessage.Performative.SIGNED_TRANSACTION, + ), + ("", ""), + signed_transaction=SignedTransaction( + "ledger_id", body={"hash": "test"} + ), + ) + ) + try: + gen.send( + LedgerApiMessage( + cast( + LedgerApiMessage.Performative, + LedgerApiMessage.Performative.ERROR, + ), + ("", ""), + message=message, + ) + ) + raise ValueError("Generator was expected to have reached its end!") + except StopIteration as e: + tx_hash, status = e.value + + assert tx_hash == "test" + assert status == expected_rpc_status + + @mock.patch.object(BaseBehaviour, "_send_transaction_signing_request") + @mock.patch.object(BaseBehaviour, "_send_transaction_request") + @mock.patch.object(BaseBehaviour, "_send_transaction_receipt_request") + @mock.patch.object(behaviour_utils, "Terms") + def test_send_raw_transaction_hashes_mismatch(self, *_: Any) -> None: + """Test 'send_raw_transaction' when signature and tx responses' hashes mismatch.""" + m = MagicMock() + gen = self.behaviour.send_raw_transaction(m) + # trigger generator function + gen.send(None) + gen.send( + SigningMessage( + cast( + SigningMessage.Performative, + SigningMessage.Performative.SIGNED_TRANSACTION, + ), + ("", ""), + signed_transaction=SignedTransaction( + "ledger_id", body={"hash": "signed"} + ), + ) + ) + try: + gen.send( + LedgerApiMessage( + cast( + LedgerApiMessage.Performative, + LedgerApiMessage.Performative.TRANSACTION_DIGEST, + ), + ("", ""), + transaction_digest=TransactionDigest("ledger_id", body="tx"), + ) + ) + raise ValueError("Generator was expected to have reached its end!") + except StopIteration as e: + tx_hash, status = e.value + + assert tx_hash is None + assert status == RPCResponseStatus.UNCLASSIFIED_ERROR + + def test_get_transaction_receipt(self, caplog: LogCaptureFixture) -> None: + """Test get_transaction_receipt.""" + + expected: JSONLike = {"dummy": "tx_receipt"} + transaction_receipt = LedgerApiMessage.TransactionReceipt("", expected, {}) + tx_receipt_message = LedgerApiMessage( + LedgerApiMessage.Performative.TRANSACTION_RECEIPT, # type: ignore + transaction_receipt=transaction_receipt, + ) + side_effect = mock_yield_and_return(tx_receipt_message) + with as_context( + mock.patch.object(self.behaviour, "_send_transaction_receipt_request"), + mock.patch.object( + self.behaviour, "wait_for_message", side_effect=side_effect + ), + ): + gen = self.behaviour.get_transaction_receipt("tx_digest") + try: + while True: + next(gen) + except StopIteration as e: + assert e.value == expected + + def test_get_transaction_receipt_error(self, caplog: LogCaptureFixture) -> None: + """Test get_transaction_receipt with error performative.""" + + error_message = LedgerApiMessage(LedgerApiMessage.Performative.ERROR, code=0) # type: ignore + side_effect = mock_yield_and_return(error_message) + with as_context( + mock.patch.object(self.behaviour, "_send_transaction_receipt_request"), + mock.patch.object( + self.behaviour, "wait_for_message", side_effect=side_effect + ), + ): + gen = self.behaviour.get_transaction_receipt("tx_digest") + try_send(gen) + try_send(gen) + assert "Error when requesting transaction receipt" in caplog.text + + @pytest.mark.parametrize("contract_address", [None, "contract_address"]) + def test_get_contract_api_response(self, contract_address: Optional[str]) -> None: + """Test 'get_contract_api_response'.""" + with mock.patch.object( + self.behaviour.context.contract_api_dialogues, + "create", + return_value=(MagicMock(), MagicMock()), + ), mock.patch.object(behaviour_utils, "Terms"), mock.patch.object( + BaseBehaviour, "_send_transaction_signing_request" + ), mock.patch.object( + BaseBehaviour, "_send_transaction_request" + ): + gen = self.behaviour.get_contract_api_response( + MagicMock(), contract_address, "contract_id", "contract_callable" + ) + # first trigger + try_send(gen, obj=None) + # wait for message + try_send(gen, obj=MagicMock()) + + @mock.patch.object( + BaseBehaviour, "_build_http_request_message", return_value=(None, None) + ) + def test_get_status(self, _: mock.Mock) -> None: + """Test '_get_status'.""" + expected_result = json.dumps("Test result.").encode() + + def dummy_do_request(*_: Any) -> Generator[None, None, MagicMock]: + """Dummy `_do_request` method.""" + yield + return mock.MagicMock(body=expected_result) + + with mock.patch.object( + BaseBehaviour, "_do_request", side_effect=dummy_do_request + ): + get_status_generator = self.behaviour._get_status() + next(get_status_generator) + with pytest.raises(StopIteration) as e: + next(get_status_generator) + res = e.value.args[0] + assert isinstance(res, MagicMock) + assert res.body == expected_result + + def test_get_netinfo(self) -> None: + """Test _get_netinfo method""" + dummy_res = { + "result": { + "n_peers": "1", + } + } + expected_result = json.dumps(dummy_res).encode() + + def dummy_do_request(*_: Any) -> Generator[None, None, MagicMock]: + """Dummy `_do_request` method.""" + yield + return mock.MagicMock(body=expected_result) + + with mock.patch.object( + BaseBehaviour, "_do_request", side_effect=dummy_do_request + ): + get_netinfo_generator = self.behaviour._get_netinfo() + next(get_netinfo_generator) + with pytest.raises(StopIteration) as e: + next(get_netinfo_generator) + res = e.value.args[0] + assert isinstance(res, MagicMock) + assert res.body == expected_result + + @pytest.mark.parametrize( + ("num_peers", "expected_num_peers", "netinfo_status_code"), + [ + ("0", 1, 200), + ("0", None, 500), + ("0", None, None), + (None, None, 200), + ], + ) + def test_num_active_peers( + self, + num_peers: Optional[str], + expected_num_peers: Optional[int], + netinfo_status_code: Optional[int], + ) -> None: + """Test num_active_peers.""" + dummy_res = { + "result": { + "n_peers": num_peers, + } + } + + def dummy_get_netinfo(*_: Any) -> Generator[None, None, MagicMock]: + """Dummy `_get_netinfo` method.""" + yield + + if netinfo_status_code is None: + raise TimeoutException() + + return mock.MagicMock( + status_code=netinfo_status_code, + body=json.dumps(dummy_res).encode(), + ) + + with mock.patch.object( + BaseBehaviour, + "_get_netinfo", + side_effect=dummy_get_netinfo, + ): + num_active_peers_generator = self.behaviour.num_active_peers() + next(num_active_peers_generator) + with pytest.raises(StopIteration) as e: + next(num_active_peers_generator) + actual_num_peers = e.value.value + assert actual_num_peers == expected_num_peers + + def test_default_callback_request_stopped(self) -> None: + """Test 'default_callback_request' when stopped.""" + message = MagicMock() + current_behaviour = self.behaviour + with mock.patch.object(self.behaviour.context.logger, "debug") as debug_mock: + self.behaviour.get_callback_request()(message, current_behaviour) + debug_mock.assert_called_with( + "Dropping message as behaviour has stopped: %s", message + ) + + def test_default_callback_late_arriving_message(self, *_: Any) -> None: + """Test 'default_callback_request' when a message arrives late.""" + self.behaviour._AsyncBehaviour__stopped = False + message = MagicMock() + current_behaviour = MagicMock() + with mock.patch.object(self.behaviour.context.logger, "warning") as info_mock: + self.behaviour.get_callback_request()(message, current_behaviour) + info_mock.assert_called_with( + "No callback defined for request with nonce: " + f"{message.dialogue_reference.__getitem__()}, " + f"arriving for behaviour: {self.behaviour.behaviour_id}" + ) + + def test_default_callback_request_waiting_message(self, *_: Any) -> None: + """Test 'default_callback_request' when waiting message.""" + self.behaviour._AsyncBehaviour__stopped = False # type: ignore + self.behaviour._AsyncBehaviour__state = ( # type: ignore + AsyncBehaviour.AsyncState.WAITING_MESSAGE + ) + message = MagicMock() + current_behaviour = self.behaviour + self.behaviour.get_callback_request()(message, current_behaviour) + + def test_default_callback_request_else(self, *_: Any) -> None: + """Test 'default_callback_request' else branch.""" + self.behaviour._AsyncBehaviour__stopped = False # type: ignore + message = MagicMock() + current_behaviour = self.behaviour + with mock.patch.object( + self.behaviour.context.logger, "warning" + ) as warning_mock: + self.behaviour.get_callback_request()(message, current_behaviour) + warning_mock.assert_called_with( + "Could not send message to FSMBehaviour: %s", message + ) + + def test_stop(self) -> None: + """Test the stop method.""" + self.behaviour.stop() + + @pytest.mark.parametrize( + "performative", + ( + TendermintMessage.Performative.GET_GENESIS_INFO, + TendermintMessage.Performative.GET_RECOVERY_PARAMS, + ), + ) + @pytest.mark.parametrize( + "address_to_acn_deliverable, n_pending", + ( + ({}, 0), + ({i: None for i in range(3)}, 3), + ({0: "test", 1: None, 2: None}, 2), + ({i: "test" for i in range(3)}, 0), + ), + ) + def test_acn_request_from_pending( + self, + performative: TendermintMessage.Performative, + address_to_acn_deliverable: Dict[str, Any], + n_pending: int, + ) -> None: + """Test the `_acn_request_from_pending` method.""" + self.behaviour.context.state.address_to_acn_deliverable = ( + address_to_acn_deliverable + ) + gen = self.behaviour._acn_request_from_pending(performative) + + if n_pending == 0: + with pytest.raises(StopIteration): + next(gen) + return + + with mock.patch.object( + self.behaviour.context.tendermint_dialogues, + "create", + return_value=(MagicMock(), MagicMock()), + ) as dialogues_mock: + dialogues_mock.assert_not_called() + self.behaviour.context.outbox.put_message = MagicMock() + self.behaviour.context.outbox.put_message.assert_not_called() + + next(gen) + + dialogues_expected_calls = tuple( + mock.call(counterparty=address, performative=performative) + for address, deliverable in address_to_acn_deliverable.items() + if deliverable is None + ) + dialogues_mock.assert_has_calls(dialogues_expected_calls) + assert self.behaviour.context.outbox.put_message.call_count == len( + dialogues_expected_calls + ) + + time.sleep(self.behaviour.params.sleep_time) + with pytest.raises(StopIteration): + next(gen) + + @pytest.mark.parametrize( + "performative", + ( + TendermintMessage.Performative.GET_GENESIS_INFO, + TendermintMessage.Performative.GET_RECOVERY_PARAMS, + ), + ) + @pytest.mark.parametrize( + "address_to_acn_deliverable_per_attempt, expected_result", + ( + ( + tuple({"address": None} for _ in range(10)), + None, + ), # an example in which no agent responds + ( + ( + {f"address{i}": None for i in range(3)}, + {"address1": None, "address2": "test", "address3": None}, + ) + + tuple( + {"address1": None, "address2": "test", "address3": "malicious"} + for _ in range(8) + ), + None, + ), # an example in which no majority is reached + ( + tuple({f"address{i}": None for i in range(3)} for _ in range(3)) + + ({"address1": "test", "address2": "test", "address3": None},), + "test", + ), # an example in which majority is reached during the 4th ACN attempt + ), + ) + def test_perform_acn_request( + self, + performative: TendermintMessage.Performative, + address_to_acn_deliverable_per_attempt: Tuple[Dict[str, Any], ...], + expected_result: Any, + ) -> None: + """Test the `_perform_acn_request` method.""" + final_attempt_idx = len(address_to_acn_deliverable_per_attempt) - 1 + gen = self.behaviour._perform_acn_request(performative) + + with mock.patch.object( + self.behaviour, + "_acn_request_from_pending", + side_effect=dummy_generator_wrapper(), + ) as _acn_request_from_pending_mock: + for i in range(self.behaviour.params.max_attempts): + acn_result = expected_result if i == final_attempt_idx + 1 else None + with mock.patch.object( + self.behaviour.context.state, + "get_acn_result", + return_value=acn_result, + ): + if i != final_attempt_idx + 1: + self.behaviour.context.state.address_to_acn_deliverable = ( + address_to_acn_deliverable_per_attempt[i] + ) + next(gen) + continue + + try: + next(gen) + except StopIteration as exc: + assert exc.value == expected_result + else: + raise AssertionError( + "The `_perform_acn_request` was expected to yield for the last time." + ) + + break + + n_expected_calls = final_attempt_idx + 1 + expected_calls = tuple( + mock.call(performative) for _ in range(n_expected_calls) + ) + assert _acn_request_from_pending_mock.call_count == n_expected_calls + _acn_request_from_pending_mock.assert_has_calls(expected_calls) + + @pytest.mark.parametrize("expected_result", (True, False)) + def test_request_recovery_params(self, expected_result: bool) -> None: + """Test `request_recovery_params`.""" + acn_result = "not None ACN result" if expected_result else None + request_recovery_params = self.behaviour.request_recovery_params(False) + + with mock.patch.object( + self.behaviour, + "_perform_acn_request", + side_effect=dummy_generator_wrapper(acn_result), + ) as perform_acn_request_mock: + next(request_recovery_params) + + try: + next(request_recovery_params) + except StopIteration as exc: + assert exc.value is expected_result + else: + raise AssertionError( + "The `request_recovery_params` was expected to yield for the last time." + ) + + perform_acn_request_mock.assert_called_once_with( + TendermintMessage.Performative.GET_RECOVERY_PARAMS + ) + + def test_start_reset(self) -> None: + """Test the `_start_reset` method.""" + with mock.patch.object( + BaseBehaviour, + "wait_from_last_timestamp", + new_callable=lambda *_: dummy_generator_wrapper(), + ): + res = self.behaviour._start_reset() + for _ in range(2): + next(res) + assert self.behaviour._check_started is not None + assert self.behaviour._check_started <= datetime.now() + assert self.behaviour._timeout == self.behaviour.params.max_healthcheck + assert not self.behaviour._is_healthy + + def test_end_reset(self) -> None: + """Test the `_end_reset` method.""" + self.behaviour._end_reset() + assert self.behaviour._check_started is None + assert self.behaviour._timeout == -1.0 + assert self.behaviour._is_healthy + + @pytest.mark.parametrize( + "check_started, is_healthy, timeout, expiration_expected", + ( + (None, True, 0, False), + (None, False, 0, False), + (datetime(1, 1, 1), True, 0, False), + (datetime.now(), False, 3000, False), + (datetime(1, 1, 1), False, 0, True), + ), + ) + def test_is_timeout_expired( + self, + check_started: Optional[datetime], + is_healthy: bool, + timeout: float, + expiration_expected: bool, + ) -> None: + """Test the `_is_timeout_expired` method.""" + self.behaviour._check_started = check_started + self.behaviour._is_healthy = is_healthy + self.behaviour._timeout = timeout + assert self.behaviour._is_timeout_expired() == expiration_expected + + @pytest.mark.parametrize("default", (True, False)) + @given( + st.datetimes( + min_value=MIN_DATETIME_WINDOWS, + max_value=MAX_DATETIME_WINDOWS, + ), + st.integers(), + st.integers(), + st.integers(), + ) + def test_get_reset_params( + self, + default: bool, + timestamp: datetime, + height: int, + interval: int, + period: int, + ) -> None: + """Test `_get_reset_params` method.""" + self.context_mock.state.round_sequence.last_round_transition_timestamp = ( + timestamp + ) + self.context_mock.state.round_sequence.last_round_transition_tm_height = height + self.behaviour.params.reset_pause_duration = interval + self.context_state_synchronized_data_mock.period_count = period + + actual = self.behaviour._get_reset_params(default) + + if default: + assert actual is None + + else: + initial_height = INITIAL_HEIGHT + genesis_time = timestamp.astimezone(pytz.UTC).strftime(GENESIS_TIME_FMT) + period_count = str(period) + + expected = { + "genesis_time": genesis_time, + "initial_height": initial_height, + "period_count": period_count, + } + + assert actual == expected + + @mock.patch.object(BaseBehaviour, "_start_reset") + @mock.patch.object(BaseBehaviour, "_is_timeout_expired") + def test_reset_tendermint_with_wait_timeout_expired(self, *_: mock.Mock) -> None: + """Test tendermint reset.""" + with pytest.raises(RuntimeError, match="Error resetting tendermint node."): + next(self.behaviour.reset_tendermint_with_wait()) + + @mock.patch.object(BaseBehaviour, "_start_reset") + @mock.patch.object( + BaseBehaviour, "_build_http_request_message", return_value=(None, None) + ) + @pytest.mark.parametrize( + "reset_response, status_response, local_height, on_startup, n_iter, expecting_success", + ( + ( + {"message": "Tendermint reset was successful.", "status": True}, + {"result": {"sync_info": {"latest_block_height": 1}}}, + 1, + False, + 3, + True, + ), + ( + {"message": "Tendermint reset was successful.", "status": True}, + {"result": {"sync_info": {"latest_block_height": 1}}}, + 1, + True, + 2, + True, + ), + ( + { + "message": "Tendermint reset was successful.", + "status": True, + "is_replay": True, + }, + {"result": {"sync_info": {"latest_block_height": 1}}}, + 1, + False, + 3, + True, + ), + ( + {"message": "Tendermint reset was successful.", "status": True}, + {"result": {"sync_info": {"latest_block_height": 1}}}, + 3, + False, + 3, + False, + ), + ( + {"message": "Error resetting tendermint.", "status": False}, + {}, + 0, + False, + 2, + False, + ), + ("wrong_response", {}, 0, False, 2, False), + ( + {"message": "Reset Successful.", "status": True}, + "not_accepting_txs_yet", + 0, + False, + 3, + False, + ), + ), + ) + def test_reset_tendermint_with_wait( + self, + build_http_request_message_mock: mock.Mock, + _start_reset: mock.Mock, + reset_response: Union[Dict[str, Union[bool, str]], str], + status_response: Union[Dict[str, Union[int, str]], str], + local_height: int, + on_startup: bool, + n_iter: int, + expecting_success: bool, + ) -> None: + """Test tendermint reset.""" + + def dummy_do_request(*_: Any) -> Generator[None, None, MagicMock]: + """Dummy `_do_request` method.""" + yield + if reset_response == "wrong_response": + return mock.MagicMock(body=b"") + return mock.MagicMock(body=json.dumps(reset_response).encode()) + + def dummy_get_status(*_: Any) -> Generator[None, None, MagicMock]: + """Dummy `_get_status` method.""" + yield + if status_response == "not_accepting_txs_yet": + return mock.MagicMock(body=b"") + return mock.MagicMock(body=json.dumps(status_response).encode()) + + period_count_mock = MagicMock() + self.context_state_synchronized_data_mock.period_count = period_count_mock + self.behaviour.params.reset_pause_duration = 1 + with mock.patch.object( + BaseBehaviour, "_is_timeout_expired", return_value=False + ), mock.patch.object( + BaseBehaviour, + "wait_from_last_timestamp", + new_callable=lambda *_: dummy_generator_wrapper(), + ), mock.patch.object( + BaseBehaviour, "_do_request", new_callable=lambda *_: dummy_do_request + ), mock.patch.object( + BaseBehaviour, "_get_status", new_callable=lambda *_: dummy_get_status + ), mock.patch.object( + BaseBehaviour, "sleep", new_callable=lambda *_: dummy_generator_wrapper() + ): + self.behaviour.context.state.round_sequence.height = local_height + reset = self.behaviour.reset_tendermint_with_wait(on_startup=on_startup) + for _ in range(n_iter): + next(reset) + initial_height = INITIAL_HEIGHT + genesis_time = self.behaviour.context.state.round_sequence.last_round_transition_timestamp.astimezone( + pytz.UTC + ).strftime( + "%Y-%m-%dT%H:%M:%S.%fZ" + ) + + expected_parameters = ( + { + "genesis_time": genesis_time, + "initial_height": initial_height, + "period_count": str(period_count_mock), + } + if not on_startup + else None + ) + + build_http_request_message_mock.assert_called_with( + "GET", + self.behaviour.context.params.tendermint_com_url + "/hard_reset", + parameters=expected_parameters, + ) + + should_be_healthy = isinstance(reset_response, dict) and reset_response.get( + "status", False + ) + assert self.behaviour._is_healthy is should_be_healthy + + # perform the last iteration which also returns the result + try: + next(reset) + except StopIteration as e: + assert e.value == expecting_success + if expecting_success: + # upon having a successful reset we expect the reset params of that + # reset to be stored in the shared state, as they could be used + # later for performing hard reset in cases when the agent <-> tendermint + # communication is broken + shared_state = cast(SharedState, self.behaviour.context.state) + tm_recovery_params = shared_state.tm_recovery_params + assert tm_recovery_params.reset_params == expected_parameters + assert ( + tm_recovery_params.round_count + == shared_state.synchronized_data.db.round_count - 1 + ) + assert ( + tm_recovery_params.reset_from_round + == self.behaviour.matching_round.auto_round_id() + ) + assert not self.behaviour._is_healthy + else: + pytest.fail("`reset_tendermint_with_wait` did not finish!") + + @given(st.binary()) + def test_fuzz_submit_tx(self, input_bytes: bytes) -> None: + """Fuzz '_submit_tx'. + + Mock context manager decorators don't work here. + + :param input_bytes: fuzz input + """ + self.behaviour._submit_tx(input_bytes) + + +def test_degenerate_behaviour_async_act() -> None: + """Test DegenerateBehaviour.async_act.""" + + class ConcreteDegenerateBehaviour(DegenerateBehaviour): + """Concrete DegenerateBehaviour class.""" + + behaviour_id = "concrete_degenerate_behaviour" + matching_round = MagicMock() + sleep_time_before_exit = 0.01 + + context = MagicMock() + # this is needed to trigger execution of async_act + context.state.round_sequence.syncing_up = False + context.state.round_sequence.block_stall_deadline_expired = False + behaviour = ConcreteDegenerateBehaviour( + name=ConcreteDegenerateBehaviour.auto_behaviour_id(), skill_context=context + ) + with pytest.raises( + SystemExit, + ): + behaviour.act() + time.sleep(0.02) + behaviour.act() + + +def test_make_degenerate_behaviour() -> None: + """Test 'make_degenerate_behaviour'.""" + + class FinalRound(DegenerateRound, ABC): + """A final round for testing.""" + + new_cls = make_degenerate_behaviour(FinalRound) + + assert isinstance(new_cls, type) + assert issubclass(new_cls, DegenerateBehaviour) + assert new_cls.matching_round == FinalRound + + assert ( + new_cls.auto_behaviour_id() + == f"degenerate_behaviour_{FinalRound.auto_round_id()}" + ) + + +class TestTmManager: + """Class to test the TmManager behaviour.""" + + _DUMMY_CONSENSUS_THRESHOLD = 3 + + def setup(self) -> None: + """Set up the tests.""" + self.context_mock = MagicMock() + self.context_params_mock = MagicMock( + request_timeout=_DEFAULT_REQUEST_TIMEOUT, + request_retry_delay=_DEFAULT_REQUEST_RETRY_DELAY, + tx_timeout=_DEFAULT_TX_TIMEOUT, + max_attempts=_DEFAULT_TX_MAX_ATTEMPTS, + consensus_params=MagicMock( + consensus_threshold=self._DUMMY_CONSENSUS_THRESHOLD + ), + ) + self.context_state_synchronized_data_mock = MagicMock( + consensus_threshold=self._DUMMY_CONSENSUS_THRESHOLD + ) + self.context_mock.params = self.context_params_mock + self.context_mock.state.synchronized_data = ( + self.context_state_synchronized_data_mock + ) + self.recovery_params = TendermintRecoveryParams(MagicMock()) + self.context_mock.state.tm_recovery_params = self.recovery_params + self.context_mock.state.round_sequence.current_round_id = "round_a" + self.context_mock.state.round_sequence.syncing_up = False + self.context_mock.state.round_sequence.block_stall_deadline_expired = False + self.context_mock.http_dialogues = HttpDialogues() + self.context_mock.handlers.__dict__ = {"http": MagicMock()} + self.tm_manager = TmManager(name="", skill_context=self.context_mock) + self.tm_manager._max_reset_retry = 1 + self.tm_manager.synchronized_data.max_participants = 3 # type: ignore + + def test_async_act(self) -> None: + """Test the async_act method of the TmManager.""" + self.tm_manager.act_wrapper() + with pytest.raises( + SystemExit, + ): + self.tm_manager.act_wrapper() + + @given(latest_block_height=st.integers(min_value=0)) + @pytest.mark.parametrize( + "acn_communication_success", + ( + True, + False, + ), + ) + @pytest.mark.parametrize( + "gentle_reset_attempted", + ( + True, + False, + ), + ) + @pytest.mark.parametrize( + ("tm_reset_success", "num_active_peers"), + [ + (True, 4), + (False, 4), + (True, 2), + (False, None), + ], + ) + def test_handle_unhealthy_tm( + self, + latest_block_height: int, + acn_communication_success: bool, + gentle_reset_attempted: bool, + tm_reset_success: bool, + num_active_peers: Optional[int], + ) -> None: + """Test _handle_unhealthy_tm.""" + + self.tm_manager.gentle_reset_attempted = gentle_reset_attempted + self.tm_manager.context.state.round_sequence.height = latest_block_height + + def mock_sleep(_seconds: int) -> Generator: + """A method that mocks sleep.""" + return + yield + + def dummy_do_request(*_: Any) -> Generator[None, None, MagicMock]: + """Dummy `_do_request` method.""" + yield + return mock.MagicMock() + + def dummy_get_status(*_: Any) -> Generator[None, None, MagicMock]: + """Dummy `_get_status` method.""" + yield + return mock.MagicMock( + body=json.dumps( + { + "result": { + "sync_info": {"latest_block_height": latest_block_height} + } + } + ).encode() + ) + + gen = self.tm_manager._handle_unhealthy_tm() + with mock.patch.object( + self.tm_manager, + "reset_tendermint_with_wait", + side_effect=yield_and_return_bool_wrapper(tm_reset_success), + ), mock.patch.object( + self.tm_manager, + "num_active_peers", + side_effect=yield_and_return_int_wrapper(num_active_peers), + ), mock.patch.object( + self.tm_manager, "sleep", side_effect=mock_sleep + ), mock.patch.object( + BaseBehaviour, + "request_recovery_params", + side_effect=dummy_generator_wrapper(acn_communication_success), + ), mock.patch.object( + BaseBehaviour, "_do_request", new_callable=lambda *_: dummy_do_request + ), mock.patch.object( + BaseBehaviour, "_get_status", new_callable=lambda *_: dummy_get_status + ), mock.patch.object( + self.tm_manager.round_sequence, "set_block_stall_deadline" + ) as set_block_stall_deadline_mock: + next(gen) + + if not gentle_reset_attempted: + next(gen) + assert self.tm_manager.gentle_reset_attempted + with pytest.raises(StopIteration): + next(gen) + set_block_stall_deadline_mock.assert_called_once() + assert self.tm_manager.gentle_reset_attempted + return + + if not acn_communication_success: + with pytest.raises(StopIteration): + next(gen) + set_block_stall_deadline_mock.assert_not_called() + return + + next(gen) + with pytest.raises(StopIteration): + next(gen) + + set_block_stall_deadline_mock.assert_not_called() + assert self.tm_manager.informed is True + + @pytest.mark.parametrize( + "n_repetitions", + ( + 1, + 2, + 1000, + ), + ) + def test_handle_unhealthy_tm_logging(self, n_repetitions: int) -> None: + """Verify if unintended logging repetition occurs during the execution of `_handle_unhealthy_tm`.""" + + self.tm_manager.gentle_reset_attempted = False + self.tm_manager.context.state.round_sequence.height = 10 + + def mock_sleep(_seconds: int) -> Generator: + """A method that mocks sleep.""" + return + yield + + def dummy_do_request(*_: Any) -> Generator[None, None, MagicMock]: + """Dummy `_do_request` method.""" + yield + return mock.MagicMock() + + def dummy_get_status(*_: Any) -> Generator[None, None, MagicMock]: + """Dummy `_get_status` method.""" + yield + return mock.MagicMock( + body=json.dumps( + {"result": {"sync_info": {"latest_block_height": 0}}} + ).encode() + ) + + with mock.patch.object( + self.tm_manager, + "reset_tendermint_with_wait", + side_effect=yield_and_return_bool_wrapper(True), + ), mock.patch.object( + self.tm_manager, + "num_active_peers", + side_effect=yield_and_return_int_wrapper(4), + ), mock.patch.object( + self.tm_manager, "sleep", side_effect=mock_sleep + ), mock.patch.object( + BaseBehaviour, "_do_request", new_callable=lambda *_: dummy_do_request + ), mock.patch.object( + BaseBehaviour, "_get_status", new_callable=lambda *_: dummy_get_status + ): + assert self.tm_manager.informed is False + for _ in range(n_repetitions): + gen = self.tm_manager._handle_unhealthy_tm() + next(gen) + assert self.tm_manager.informed is True + + @pytest.mark.parametrize( + "expected_reset_params", + ( + {"genesis_time": "genesis-time", "initial_height": "1"}, + None, + ), + ) + def test_get_reset_params( + self, expected_reset_params: Optional[Dict[str, str]] + ) -> None: + """Test that reset params returns the correct params.""" + self.context_mock.state.tm_recovery_params = TendermintRecoveryParams( + reset_from_round="does not matter", reset_params=expected_reset_params + ) + actual_reset_params = self.tm_manager._get_reset_params(False) + assert expected_reset_params == actual_reset_params + + # setting the "default" arg to true should have no effect + actual_reset_params = self.tm_manager._get_reset_params(True) + assert expected_reset_params == actual_reset_params + + def test_sleep_after_hard_reset(self) -> None: + """Check that hard_reset_sleep returns the expected amount of time.""" + expected = self.tm_manager._hard_reset_sleep + actual = self.tm_manager.hard_reset_sleep + assert actual == expected + + @pytest.mark.parametrize( + ("state", "notified", "message", "num_iter"), + [ + (AsyncBehaviour.AsyncState.READY, False, None, 1), + (AsyncBehaviour.AsyncState.WAITING_MESSAGE, True, Message(), 2), + (AsyncBehaviour.AsyncState.WAITING_MESSAGE, True, Message(), 1), + ], + ) + def test_try_fix( + self, + state: AsyncBehaviour.AsyncState, + notified: bool, + message: Optional[Message], + num_iter: int, + ) -> None: + """Tests try_fix.""" + + def mock_handle_unhealthy_tm() -> Generator: + """A mock implementation of _handle_unhealthy_tm.""" + for _ in range(num_iter): + msg = yield + if msg is not None: + # if a message is recieved, the state of the behviour should be "RUNNING" + self.tm_manager._AsyncBehaviour__state = ( + AsyncBehaviour.AsyncState.RUNNING + ) + return + + with mock.patch.object( + self.tm_manager, + "_handle_unhealthy_tm", + side_effect=mock_handle_unhealthy_tm, + ): + # there is no active generator in the beginning + assert not self.tm_manager.is_acting + + # a generator should be created, and be active + self.tm_manager.try_fix() + assert self.tm_manager.is_acting + + # a message may (or may not) arrive + self.tm_manager._AsyncBehaviour__notified = notified + self.tm_manager._AsyncBehaviour__state = state + self.tm_manager._AsyncBehaviour__message = message + + # the generator has a single yield statement, + # a second try_fix() call should finish it + for _ in range(num_iter): + self.tm_manager.try_fix() + assert not self.tm_manager.is_acting, num_iter + + @pytest.mark.parametrize( + "state", + [ + AsyncBehaviour.AsyncState.WAITING_MESSAGE, + AsyncBehaviour.AsyncState.READY, + ], + ) + def test_get_callback_request(self, state: AsyncBehaviour.AsyncState) -> None: + """Tests get_callback_request.""" + self.tm_manager._AsyncBehaviour__state = state + dummy_msg, dummy_behaviour = MagicMock(), MagicMock() + callback_req = self.tm_manager.get_callback_request() + with mock.patch.object(self.tm_manager, "try_send"): + callback_req(dummy_msg, dummy_behaviour) + + def test_is_acting(self) -> None: + """Test is_acting.""" + self.tm_manager._active_generator = MagicMock() + assert self.tm_manager.is_acting + + self.tm_manager._active_generator = None + assert not self.tm_manager.is_acting + + +def test_meta_base_behaviour_when_instance_not_subclass_of_base_behaviour() -> None: + """Test instantiation of meta class when instance not a subclass of BaseBehaviour.""" + + class MyBaseBehaviour(metaclass=_MetaBaseBehaviour): + pass + + +def test_base_behaviour_instantiation_without_attributes_raises_error() -> None: + """Test that definition of concrete subclass of BaseBehaviour without attributes raises error.""" + with pytest.raises(BaseBehaviourInternalError): + + class MyBaseBehaviour(BaseBehaviour): + pass + + +class TestIPFSBehaviour: + """Test IPFSBehaviour tests.""" + + def setup(self) -> None: + """Sets up the tests.""" + self.context_mock = MagicMock() + self.context_mock.ipfs_dialogues = IpfsDialogues( + connection_id=str(IPFS_CONNECTION_ID) + ) + self.behaviour = BehaviourATest(name="", skill_context=self.context_mock) + + def test_build_ipfs_message(self) -> None: + """Tests _build_ipfs_message.""" + res = self.behaviour._build_ipfs_message(IpfsMessage.Performative.GET_FILES) # type: ignore + assert res is not None + + def test_build_ipfs_store_file_req(self) -> None: + """Tests _build_ipfs_store_file_req.""" + with mock.patch.object( + IPFSInteract, "store", return_value=MagicMock() + ) as mock_store: + res = self.behaviour._build_ipfs_store_file_req("dummy_filename", {}) + mock_store.assert_called() + assert res is not None + + def test_build_ipfs_get_file_req(self) -> None: + """Tests _build_ipfs_get_file_req.""" + res = self.behaviour._build_ipfs_get_file_req("dummy_ipfs_hash") + assert res is not None + + def test_deserialize_ipfs_objects(self) -> None: + """Tests _deserialize_ipfs_objects""" + with mock.patch.object( + IPFSInteract, "load", return_value=MagicMock() + ) as mock_load: + res = self.behaviour._deserialize_ipfs_objects({}) + mock_load.assert_called() + assert res is not None diff --git a/packages/valory/skills/abstract_round_abci/tests/test_common.py b/packages/valory/skills/abstract_round_abci/tests/test_common.py new file mode 100644 index 0000000..427aa81 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/test_common.py @@ -0,0 +1,407 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test the common.py module of the skill.""" + +import random +import re +from typing import ( + Any, + Callable, + Dict, + FrozenSet, + Generator, + Optional, + Set, + Type, + TypeVar, + Union, +) +from unittest import mock +from unittest.mock import MagicMock + +import pytest + +from packages.valory.protocols.ledger_api.message import LedgerApiMessage +from packages.valory.skills.abstract_round_abci.common import ( + RandomnessBehaviour, + SelectKeeperBehaviour, + random_selection, +) +from packages.valory.skills.abstract_round_abci.models import BaseParams +from packages.valory.skills.abstract_round_abci.tests.conftest import irrelevant_config +from packages.valory.skills.abstract_round_abci.utils import VerifyDrand + + +ReturnValueType = TypeVar("ReturnValueType") + + +def test_random_selection() -> None: + """Test 'random_selection'""" + assert random_selection(elements=[0, 1, 2], randomness=0.25) == 0 + assert random_selection(elements=[0, 1, 2], randomness=0.5) == 1 + assert random_selection(elements=[0, 1, 2], randomness=0.75) == 2 + + with pytest.raises( + ValueError, match=re.escape("Randomness should lie in the [0,1) interval") + ): + random_selection(elements=[0, 1], randomness=-1) + + with pytest.raises( + ValueError, match=re.escape("Randomness should lie in the [0,1) interval") + ): + random_selection(elements=[0, 1], randomness=1) + + with pytest.raises( + ValueError, match=re.escape("Randomness should lie in the [0,1) interval") + ): + random_selection(elements=[0, 1], randomness=2) + + with pytest.raises(ValueError, match="No elements to randomly select among"): + random_selection(elements=[], randomness=0.5) + + +class DummyRandomnessBehaviour(RandomnessBehaviour): + """Dummy randomness behaviour.""" + + behaviour_id = "dummy_randomness" + payload_class = MagicMock() + matching_round = MagicMock() + + +class DummySelectKeeperBehaviour(SelectKeeperBehaviour): + """Dummy select keeper behaviour.""" + + behaviour_id = "dummy_select_keeper" + payload_class = MagicMock() + matching_round = MagicMock() + + +DummyBehaviourType = Union[DummyRandomnessBehaviour, DummySelectKeeperBehaviour] + + +class BaseDummyBehaviour: # pylint: disable=too-few-public-methods + """A Base dummy behaviour class.""" + + behaviour: DummyBehaviourType + dummy_behaviour_cls: Type[DummyBehaviourType] + + @classmethod + def setup_class(cls) -> None: + """Setup the test class.""" + cls.behaviour = cls.dummy_behaviour_cls( + name="test", + skill_context=MagicMock( + params=BaseParams( + name="test", + skill_context=MagicMock(), + service_id="test_id", + consensus=dict(max_participants=1), + **irrelevant_config, + ), + ), + ) + + +def dummy_generator( + return_value: ReturnValueType, +) -> Callable[[Any, Any], Generator[None, None, ReturnValueType]]: + """A method that returns a dummy generator which yields nothing once and then returns the given `return_value`.""" + + def dummy_generator_wrapped( + *_: Any, **__: Any + ) -> Generator[None, None, ReturnValueType]: + """A wrapped method which yields nothing once and then returns the given `return_value`.""" + yield + return return_value + + return dummy_generator_wrapped + + +def last_iteration(gen: Generator) -> None: + """Perform a generator iteration and ensure that it is the last one.""" + with pytest.raises(StopIteration): + next(gen) + + +class TestRandomnessBehaviour(BaseDummyBehaviour): + """Test `RandomnessBehaviour`.""" + + @classmethod + def setup_class(cls) -> None: + """Setup the test class.""" + cls.dummy_behaviour_cls = DummyRandomnessBehaviour + super().setup_class() + + @pytest.mark.parametrize( + "return_value, expected_hash", + ( + (MagicMock(performative=LedgerApiMessage.Performative.ERROR), None), + (MagicMock(state=MagicMock(body={"hash_key_is_not_in_body": ""})), None), + ( + MagicMock(state=MagicMock(body={"hash": "test_randomness"})), + { + "randomness": "d067b86fa5235e7e5225e8328e8faac5c279cbf57131d647e4da0a70df6d3d7b", + "round": 0, + }, + ), + ), + ) + def test_failsafe_randomness( + self, return_value: MagicMock, expected_hash: Optional[str] + ) -> None: + """Test `failsafe_randomness`.""" + gen = self.behaviour.failsafe_randomness() + + with mock.patch.object( + DummyRandomnessBehaviour, + "get_ledger_api_response", + dummy_generator(return_value), + ): + next(gen) + try: + next(gen) + except StopIteration as e: + assert e.value == expected_hash + else: + raise AssertionError( + "`get_ledger_api_response`'s generator should have been exhausted." + ) + + @pytest.mark.parametrize("randomness_response", ("test", None)) + @pytest.mark.parametrize("verified", (True, False)) + def test_get_randomness_from_api( + self, randomness_response: Optional[str], verified: bool + ) -> None: + """Test `get_randomness_from_api`.""" + # create a dummy `process_response` for `MagicMock`ed `randomness_api` + self.behaviour.context.randomness_api.process_response = ( + lambda res: res + "_processed" if res is not None else None + ) + gen = self.behaviour.get_randomness_from_api() + + with mock.patch.object( + DummyRandomnessBehaviour, + "get_http_response", + dummy_generator(randomness_response), + ), mock.patch.object( + VerifyDrand, + "verify", + return_value=(verified, "Error message."), + ): + next(gen) + try: + next(gen) + except StopIteration as e: + if randomness_response is None or not verified: + assert e.value is None + else: + assert e.value == randomness_response + "_processed" + else: + raise AssertionError( + "`get_randomness_from_api`'s generator should have been exhausted." + ) + + @pytest.mark.parametrize( + "retries_exceeded, failsafe_succeeds", + # (False, False) is not tested, because it does not make sense + ((True, False), (True, True), (False, True)), + ) + @pytest.mark.parametrize( + "observation", + ( + None, + {}, + { + "randomness": "d067b86fa5235e7e5225e8328e8faac5c279cbf57131d647e4da0a70df6d3d7b", + "round": 0, + }, + ), + ) + def test_async_act( + self, + retries_exceeded: bool, + failsafe_succeeds: bool, + observation: Optional[Dict[str, Union[str, int]]], + ) -> None: + """Test `async_act`.""" + # create a dummy `is_retries_exceeded` for `MagicMock`ed `randomness_api` + self.behaviour.context.randomness_api.is_retries_exceeded = ( + lambda: retries_exceeded + ) + gen = self.behaviour.async_act() + + with mock.patch.object( + self.behaviour, + "failsafe_randomness", + dummy_generator(observation), + ), mock.patch.object( + self.behaviour, + "get_randomness_from_api", + dummy_generator(observation), + ), mock.patch.object( + self.behaviour, + "send_a2a_transaction", + ) as send_a2a_transaction_mocked, mock.patch.object( + self.behaviour, + "wait_until_round_end", + ) as wait_until_round_end_mocked, mock.patch.object( + self.behaviour, + "set_done", + ) as set_done_mocked, mock.patch.object( + self.behaviour, + "sleep", + ) as sleep_mocked: + next(gen) + last_iteration(gen) + + if not failsafe_succeeds or failsafe_succeeds and observation is None: + return + + # here, the observation is retrieved from either `failsafe_randomness` or `get_randomness_from_api` + # depending on the test's parametrization + if not observation: + sleep_mocked.assert_called_once_with( + self.behaviour.context.randomness_api.retries_info.suggested_sleep_time + ) + self.behaviour.context.randomness_api.increment_retries.assert_called_once() + return + + send_a2a_transaction_mocked.assert_called_once() + wait_until_round_end_mocked.assert_called_once() + set_done_mocked.assert_called_once() + + def test_clean_up(self) -> None: + """Test `clean_up`.""" + self.behaviour.clean_up() + self.behaviour.context.randomness_api.reset_retries.assert_called_once() + + def teardown(self) -> None: + """Teardown run after each test method.""" + self.behaviour.context.randomness_api.increment_retries.reset_mock() + + +class TestSelectKeeperBehaviour(BaseDummyBehaviour): + """Tests for `SelectKeeperBehaviour`.""" + + @classmethod + def setup_class(cls) -> None: + """Setup the test class.""" + cls.dummy_behaviour_cls = DummySelectKeeperBehaviour + super().setup_class() + + @mock.patch.object(random, "shuffle", lambda do_not_shuffle: do_not_shuffle) + @pytest.mark.parametrize( + "participants, blacklisted_keepers, most_voted_keeper_address, expected_keeper", + ( + ( + frozenset((f"test_p{i}" for i in range(4))), + set(), + "test_p0", + "test_p1", + ), + ( + frozenset((f"test_p{i}" for i in range(4))), + set(), + "test_p1", + "test_p2", + ), + ( + frozenset((f"test_p{i}" for i in range(4))), + set(), + "test_p2", + "test_p3", + ), + ( + frozenset((f"test_p{i}" for i in range(4))), + set(), + "test_p3", + "test_p0", + ), + ( + frozenset((f"test_p{i}" for i in range(4))), + {f"test_p{i}" for i in range(1)}, + "test_p1", + "test_p2", + ), + ( + frozenset((f"test_p{i}" for i in range(4))), + {f"test_p{i}" for i in range(4)}, + "", + "", + ), + ), + ) + def test_select_keeper( + self, + participants: FrozenSet[str], + blacklisted_keepers: Set[str], + most_voted_keeper_address: str, # pylint: disable=unused-argument + expected_keeper: str, + ) -> None: + """Test `_select_keeper`.""" + for sync_data_name in ( + "participants", + "blacklisted_keepers", + "most_voted_keeper_address", + ): + setattr( + self.behaviour.context.state.synchronized_data, + sync_data_name, + locals()[sync_data_name], + ) + + select_keeper_method = ( + self.behaviour._select_keeper # pylint: disable=protected-access + ) + + if not participants - blacklisted_keepers: + with pytest.raises( + RuntimeError, + match="Cannot continue if all the keepers have been blacklisted!", + ): + select_keeper_method() + return + + with mock.patch("random.seed"): + actual_keeper = select_keeper_method() + assert actual_keeper == expected_keeper + + def test_async_act(self) -> None: + """Test `async_act`.""" + gen = self.behaviour.async_act() + + with mock.patch.object( + self.behaviour, + "_select_keeper", + return_value="test_keeper", + ), mock.patch.object( + self.behaviour, + "send_a2a_transaction", + ) as send_a2a_transaction_mocked, mock.patch.object( + self.behaviour, + "wait_until_round_end", + ) as wait_until_round_end_mocked, mock.patch.object( + self.behaviour, + "set_done", + ) as set_done_mocked: + last_iteration(gen) + send_a2a_transaction_mocked.assert_called_once() + wait_until_round_end_mocked.assert_called_once() + set_done_mocked.assert_called_once() diff --git a/packages/valory/skills/abstract_round_abci/tests/test_dialogues.py b/packages/valory/skills/abstract_round_abci/tests/test_dialogues.py new file mode 100644 index 0000000..12aeeaa --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/test_dialogues.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test the dialogues.py module of the skill.""" + +# pylint: skip-file + +from enum import Enum +from typing import Type, cast +from unittest.mock import MagicMock + +import pytest +from aea.protocols.dialogue.base import Dialogues +from aea.skills.base import Model + +from packages.valory.connections.ipfs.connection import PUBLIC_ID as IPFS_CONNECTION_ID +from packages.valory.protocols.ipfs import IpfsMessage +from packages.valory.skills.abstract_round_abci.dialogues import ( + AbciDialogue, + AbciDialogues, + ContractApiDialogue, + ContractApiDialogues, + HttpDialogue, + HttpDialogues, + IpfsDialogues, + LedgerApiDialogue, + LedgerApiDialogues, + SigningDialogue, + SigningDialogues, + TendermintDialogue, + TendermintDialogues, +) + + +@pytest.mark.parametrize( + "dialogues_cls,expected_role_from_first_message", + [ + (AbciDialogues, AbciDialogue.Role.CLIENT), + (HttpDialogues, HttpDialogue.Role.CLIENT), + (SigningDialogues, SigningDialogue.Role.SKILL), + (LedgerApiDialogues, LedgerApiDialogue.Role.AGENT), + (ContractApiDialogues, ContractApiDialogue.Role.AGENT), + (TendermintDialogues, TendermintDialogue.Role.AGENT), + ], +) +def test_dialogues_creation( + dialogues_cls: Type[Model], expected_role_from_first_message: Enum +) -> None: + """Test XDialogues creations.""" + dialogues = cast(Dialogues, dialogues_cls(name="", skill_context=MagicMock())) + assert expected_role_from_first_message == dialogues._role_from_first_message( + MagicMock(), MagicMock() + ) + + +def test_ledger_api_dialogue() -> None: + """Test 'LedgerApiDialogue' creation.""" + dialogue = LedgerApiDialogue(MagicMock(), "", MagicMock()) + with pytest.raises(ValueError, match="Terms not set!"): + dialogue.terms + + expected_terms = MagicMock() + dialogue.terms = expected_terms + assert expected_terms == dialogue.terms + + +def test_contract_api_dialogue() -> None: + """Test 'ContractApiDialogue' creation.""" + dialogue = ContractApiDialogue(MagicMock(), "", MagicMock()) + with pytest.raises(ValueError, match="Terms not set!"): + dialogue.terms + + expected_terms = MagicMock() + dialogue.terms = expected_terms + assert expected_terms == dialogue.terms + + +def test_ipfs_dialogue() -> None: + """Test 'IpfsDialogues' creation.""" + dialogues = IpfsDialogues(name="", skill_context=MagicMock()) + dialogues.create( + counterparty=str(IPFS_CONNECTION_ID), + performative=IpfsMessage.Performative.GET_FILES, + ) diff --git a/packages/valory/skills/abstract_round_abci/tests/test_handlers.py b/packages/valory/skills/abstract_round_abci/tests/test_handlers.py new file mode 100644 index 0000000..415f11c --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/test_handlers.py @@ -0,0 +1,596 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test the handlers.py module of the skill.""" + +# pylint: skip-file + +import json +import logging +from dataclasses import asdict +from datetime import datetime +from typing import Any, Dict, cast +from unittest import mock +from unittest.mock import MagicMock + +import pytest +from _pytest.logging import LogCaptureFixture +from aea.configurations.data_types import PublicId +from aea.protocols.base import Message + +from packages.valory.protocols.abci import AbciMessage +from packages.valory.protocols.abci.custom_types import ( + CheckTxType, + CheckTxTypeEnum, + ConsensusParams, + Evidences, + Header, + LastCommitInfo, + Timestamp, + ValidatorUpdates, +) +from packages.valory.protocols.http import HttpMessage +from packages.valory.protocols.tendermint import TendermintMessage +from packages.valory.skills.abstract_round_abci import handlers +from packages.valory.skills.abstract_round_abci.base import ( + ABCIAppInternalError, + AddBlockError, + ERROR_CODE, + OK_CODE, + SignatureNotValidError, + TransactionNotValidError, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + AbciDialogue, + AbciDialogues, + TendermintDialogue, + TendermintDialogues, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + ABCIRoundHandler, + AbstractResponseHandler, + TendermintHandler, + Transaction, + exception_to_info_msg, +) +from packages.valory.skills.abstract_round_abci.models import TendermintRecoveryParams +from packages.valory.skills.abstract_round_abci.test_tools.rounds import DummyRound + + +def test_exception_to_info_msg() -> None: + """Test 'exception_to_info_msg' helper function.""" + exception = Exception("exception message") + expected_string = f"{exception.__class__.__name__}: {str(exception)}" + actual_string = exception_to_info_msg(exception) + assert expected_string == actual_string + + +class TestABCIRoundHandler: + """Test 'ABCIRoundHandler'.""" + + def setup(self) -> None: + """Set up the tests.""" + self.context = MagicMock(skill_id=PublicId.from_str("dummy/skill:0.1.0")) + self.dialogues = AbciDialogues(name="", skill_context=self.context) + self.handler = ABCIRoundHandler(name="", skill_context=self.context) + self.context.state.round_sequence.height = 0 + self.context.state.round_sequence.root_hash = b"root_hash" + self.context.state.round_sequence.last_round_transition_timestamp = ( + datetime.now() + ) + + def test_info(self) -> None: + """Test the 'info' handler method.""" + message, dialogue = self.dialogues.create( + counterparty="", + performative=AbciMessage.Performative.REQUEST_INFO, + version="", + block_version=0, + p2p_version=0, + ) + response = self.handler.info( + cast(AbciMessage, message), cast(AbciDialogue, dialogue) + ) + assert response.performative == AbciMessage.Performative.RESPONSE_INFO + + @pytest.mark.parametrize("app_hash", (b"", b"test")) + def test_init_chain(self, app_hash: bytes) -> None: + """Test the 'init_chain' handler method.""" + time = Timestamp(0, 0) + consensus_params = ConsensusParams(*(mock.MagicMock() for _ in range(4))) + validators = ValidatorUpdates(mock.MagicMock()) + message, dialogue = self.dialogues.create( + counterparty="", + performative=AbciMessage.Performative.REQUEST_INIT_CHAIN, + time=time, + chain_id="test_chain_id", + consensus_params=consensus_params, + validators=validators, + app_state_bytes=b"", + initial_height=10, + ) + self.context.state.round_sequence.last_round_transition_root_hash = app_hash + response = self.handler.init_chain( + cast(AbciMessage, message), cast(AbciDialogue, dialogue) + ) + assert response.performative == AbciMessage.Performative.RESPONSE_INIT_CHAIN + assert response.validators == ValidatorUpdates([]) + assert response.app_hash == app_hash + + def test_begin_block(self) -> None: + """Test the 'begin_block' handler method.""" + header = Header(*(MagicMock() for _ in range(14))) + last_commit_info = LastCommitInfo(*(MagicMock() for _ in range(2))) + byzantine_validators = Evidences(MagicMock()) + message, dialogue = self.dialogues.create( + counterparty="", + performative=AbciMessage.Performative.REQUEST_BEGIN_BLOCK, + hash=b"", + header=header, + last_commit_info=last_commit_info, + byzantine_validators=byzantine_validators, + ) + response = self.handler.begin_block( + cast(AbciMessage, message), cast(AbciDialogue, dialogue) + ) + assert response.performative == AbciMessage.Performative.RESPONSE_BEGIN_BLOCK + + @mock.patch.object(handlers, "Transaction") + def test_check_tx(self, *_: Any) -> None: + """Test the 'check_tx' handler method.""" + message, dialogue = self.dialogues.create( + counterparty="", + performative=AbciMessage.Performative.REQUEST_CHECK_TX, + tx=b"", + type=CheckTxType(CheckTxTypeEnum.NEW), + ) + response = self.handler.check_tx( + cast(AbciMessage, message), cast(AbciDialogue, dialogue) + ) + assert response.performative == AbciMessage.Performative.RESPONSE_CHECK_TX + assert response.code == OK_CODE + + @mock.patch.object( + Transaction, + "decode", + side_effect=SignatureNotValidError, + ) + def test_check_tx_negative(self, *_: Any) -> None: + """Test the 'check_tx' handler method, negative case.""" + message, dialogue = self.dialogues.create( + counterparty="", + performative=AbciMessage.Performative.REQUEST_CHECK_TX, + tx=b"", + type=CheckTxType(CheckTxTypeEnum.NEW), + ) + response = self.handler.check_tx( + cast(AbciMessage, message), cast(AbciDialogue, dialogue) + ) + assert response.performative == AbciMessage.Performative.RESPONSE_CHECK_TX + assert response.code == ERROR_CODE + + @mock.patch.object(handlers, "Transaction") + def test_deliver_tx(self, *_: Any) -> None: + """Test the 'deliver_tx' handler method.""" + message, dialogue = self.dialogues.create( + counterparty="", + performative=AbciMessage.Performative.REQUEST_DELIVER_TX, + tx=b"", + ) + with mock.patch.object( + self.context.state.round_sequence, "add_pending_offence" + ) as mock_add_pending_offence: + response = self.handler.deliver_tx( + cast(AbciMessage, message), cast(AbciDialogue, dialogue) + ) + mock_add_pending_offence.assert_called_once() + + assert response.performative == AbciMessage.Performative.RESPONSE_DELIVER_TX + assert response.code == OK_CODE + + @mock.patch.object( + Transaction, + "decode", + side_effect=SignatureNotValidError, + ) + def test_deliver_tx_negative(self, *_: Any) -> None: + """Test the 'deliver_tx' handler method, negative case.""" + message, dialogue = self.dialogues.create( + counterparty="", + performative=AbciMessage.Performative.REQUEST_DELIVER_TX, + tx=b"", + ) + with mock.patch.object( + self.context.state.round_sequence, "add_pending_offence" + ) as mock_add_pending_offence: + response = self.handler.deliver_tx( + cast(AbciMessage, message), cast(AbciDialogue, dialogue) + ) + mock_add_pending_offence.assert_not_called() + + assert response.performative == AbciMessage.Performative.RESPONSE_DELIVER_TX + assert response.code == ERROR_CODE + + @mock.patch.object(handlers, "Transaction") + def test_deliver_bad_tx(self, *_: Any) -> None: + """Test the 'deliver_tx' handler method, when the transaction is not ok.""" + message, dialogue = self.dialogues.create( + counterparty="", + performative=AbciMessage.Performative.REQUEST_DELIVER_TX, + tx=b"", + ) + with mock.patch.object( + self.context.state.round_sequence, + "check_is_finished", + side_effect=TransactionNotValidError, + ), mock.patch.object( + self.context.state.round_sequence, "add_pending_offence" + ) as mock_add_pending_offence: + response = self.handler.deliver_tx( + cast(AbciMessage, message), cast(AbciDialogue, dialogue) + ) + mock_add_pending_offence.assert_called_once() + + assert response.performative == AbciMessage.Performative.RESPONSE_DELIVER_TX + assert response.code == ERROR_CODE + + @pytest.mark.parametrize("request_height", tuple(range(3))) + def test_end_block(self, request_height: int) -> None: + """Test the 'end_block' handler method.""" + message, dialogue = self.dialogues.create( + counterparty="", + performative=AbciMessage.Performative.REQUEST_END_BLOCK, + height=request_height, + ) + assert isinstance(message, AbciMessage) + assert isinstance(dialogue, AbciDialogue) + assert message.height == request_height + assert self.context.state.round_sequence.tm_height != request_height + response = self.handler.end_block(message, dialogue) + assert response.performative == AbciMessage.Performative.RESPONSE_END_BLOCK + assert self.context.state.round_sequence.tm_height == request_height + + def test_commit(self) -> None: + """Test the 'commit' handler method.""" + message, dialogue = self.dialogues.create( + counterparty="", + performative=AbciMessage.Performative.REQUEST_COMMIT, + ) + response = self.handler.commit( + cast(AbciMessage, message), cast(AbciDialogue, dialogue) + ) + assert response.performative == AbciMessage.Performative.RESPONSE_COMMIT + + def test_commit_negative(self) -> None: + """Test the 'commit' handler method, negative case.""" + self.context.state.round_sequence.commit.side_effect = AddBlockError() + message, dialogue = self.dialogues.create( + counterparty="", + performative=AbciMessage.Performative.REQUEST_COMMIT, + ) + with pytest.raises(AddBlockError): + self.handler.commit( + cast(AbciMessage, message), cast(AbciDialogue, dialogue) + ) + + +class ConcreteResponseHandler(AbstractResponseHandler): + """A concrete response handler for testing purposes.""" + + SUPPORTED_PROTOCOL = HttpMessage.protocol_id + allowed_response_performatives = frozenset({HttpMessage.Performative.RESPONSE}) + + +class TestAbstractResponseHandler: + """Test 'AbstractResponseHandler'.""" + + def setup(self) -> None: + """Set up the tests.""" + self.context = MagicMock() + self.handler = ConcreteResponseHandler(name="", skill_context=self.context) + + def test_handle(self) -> None: + """Test the 'handle' method.""" + callback = MagicMock() + request_reference = "reference" + self.context.requests.request_id_to_callback = {} + self.context.requests.request_id_to_callback[request_reference] = callback + with mock.patch.object( + self.handler, "_recover_protocol_dialogues" + ) as mock_dialogues_fn: + mock_dialogue = MagicMock() + mock_dialogue.dialogue_label.dialogue_reference = (request_reference, "") + mock_dialogues = MagicMock() + mock_dialogues.update = MagicMock(return_value=mock_dialogue) + mock_dialogues_fn.return_value = mock_dialogues + mock_message = MagicMock(performative=HttpMessage.Performative.RESPONSE) + self.handler.handle(mock_message) + callback.assert_called() + + @mock.patch.object( + AbstractResponseHandler, "_recover_protocol_dialogues", return_value=None + ) + def test_handle_negative_cannot_recover_dialogues(self, *_: Any) -> None: + """Test the 'handle' method, negative case (cannot recover dialogues).""" + self.handler.handle(MagicMock()) + + @mock.patch.object(AbstractResponseHandler, "_recover_protocol_dialogues") + def test_handle_negative_cannot_update_dialogues( + self, mock_dialogues_fn: Any + ) -> None: + """Test the 'handle' method, negative case (cannot update dialogues).""" + mock_dialogues = MagicMock(update=MagicMock(return_value=None)) + mock_dialogues_fn.return_value = mock_dialogues + self.handler.handle(MagicMock()) + + def test_handle_negative_performative_not_allowed(self) -> None: + """Test the 'handle' method, negative case (performative not allowed).""" + self.handler.handle(MagicMock()) + + def test_handle_negative_cannot_find_callback(self) -> None: + """Test the 'handle' method, negative case (cannot find callback).""" + self.context.requests.request_id_to_callback = {} + with pytest.raises( + ABCIAppInternalError, match="No callback defined for request with nonce: " + ): + self.handler.handle( + MagicMock(performative=HttpMessage.Performative.RESPONSE) + ) + + +class TestTendermintHandler: + """Test Tendermint Handler""" + + def setup(self) -> None: + """Set up the tests.""" + self.agent_name = "Alice" + self.context = MagicMock(skill_id=PublicId.from_str("dummy/skill:0.1.0")) + other_agents = ["Alice", "Bob", "Charlie"] + self.context.state = MagicMock(acn_container=lambda: other_agents) + self.handler = TendermintHandler(name="dummy", skill_context=self.context) + self.handler.context.logger = logging.getLogger() + self.dialogues = TendermintDialogues(name="dummy", skill_context=self.context) + + @property + def dummy_validator_config(self) -> Dict[str, Dict[str, str]]: + """Dummy validator config""" + return { + self.agent_name: { + "hostname": "localhost", + "address": "address", + "pub_key": "pub_key", + "peer_id": "peer_id", + } + } + + @staticmethod + def make_error_message() -> TendermintMessage: + """Make dummy error message""" + performative = TendermintMessage.Performative.ERROR + error_code = TendermintMessage.ErrorCode.INVALID_REQUEST + error_msg, error_data = "", {} # type: ignore + message = TendermintMessage( + performative, # type: ignore + error_code=error_code, + error_msg=error_msg, + error_data=error_data, + ) + message.sender = "Alice" + return message + + # pre-condition checks + def test_handle_unidentified_tendermint_dialogue( + self, caplog: LogCaptureFixture + ) -> None: + """Test unidentified tendermint dialogue""" + message = Message() + with mock.patch.object(self.handler.dialogues, "update", return_value=None): + self.handler.handle(message) + log_message = self.handler.LogMessages.unidentified_dialogue.value + assert log_message in caplog.text + + def test_handle_no_addresses_retrieved_yet(self, caplog: LogCaptureFixture) -> None: + """Test handle request no registered addresses""" + performative = TendermintMessage.Performative.GET_GENESIS_INFO + message = TendermintMessage(performative) # type: ignore + message.sender = "Alice" + self.handler.initial_tm_configs = {} + self.handler.handle(message) + log_message = self.handler.LogMessages.no_addresses_retrieved_yet.value + assert log_message in caplog.text + log_message = self.handler.LogMessages.sending_error_response.value + assert log_message in caplog.text + + def test_handle_not_in_registered_addresses( + self, caplog: LogCaptureFixture + ) -> None: + """Test handle response sender not in registered addresses""" + performative = TendermintMessage.Performative.GENESIS_INFO + message = TendermintMessage(performative, info="info") # type: ignore + message.sender = "NotAlice" + self.handler.handle(message) + log_message = self.handler.LogMessages.not_in_registered_addresses.value + assert log_message in caplog.text + + # request + def test_handle_get_genesis_info(self, caplog: LogCaptureFixture) -> None: + """Test handle request for genesis info""" + performative = TendermintMessage.Performative.GET_GENESIS_INFO + message = TendermintMessage(performative) # type: ignore + self.context.agent_address = message.sender = self.agent_name + self.handler.initial_tm_configs = self.dummy_validator_config + self.handler.handle(message) + log_message = self.handler.LogMessages.sending_request_response.value + assert log_message in caplog.text + + # response + def test_handle_response_invalid_addresses(self, caplog: LogCaptureFixture) -> None: + """Test handle response for genesis info with invalid address.""" + validator_config = self.dummy_validator_config + validator_config[self.agent_name]["hostname"] = "random" + performative = TendermintMessage.Performative.GENESIS_INFO + info = json.dumps(validator_config[self.agent_name]) + message = TendermintMessage(performative, info=info) # type: ignore + self.context.agent_address = message.sender = self.agent_name + self.handler.initial_tm_configs = validator_config + self.handler.handle(message) + log_message = self.handler.LogMessages.failed_to_parse_address.value + assert log_message in caplog.text + + def test_handle_genesis_info(self, caplog: LogCaptureFixture) -> None: + """Test handle response for genesis info with valid address""" + performative = TendermintMessage.Performative.GENESIS_INFO + info = json.dumps(self.dummy_validator_config[self.agent_name]) + message = TendermintMessage(performative, info=info) # type: ignore + self.context.agent_address = message.sender = self.agent_name + self.handler.initial_tm_configs = self.dummy_validator_config + self.handler.handle(message) + log_message = self.handler.LogMessages.collected_config_info.value + assert log_message in caplog.text + + @pytest.mark.parametrize("registered", (True, False)) + @pytest.mark.parametrize( + "performative", + ( + TendermintMessage.Performative.RECOVERY_PARAMS, + TendermintMessage.Performative.GET_RECOVERY_PARAMS, + ), + ) + def test_recovery_params( + self, + registered: bool, + performative: TendermintMessage.Performative, + caplog: LogCaptureFixture, + ) -> None: + """Test handle response for recovery parameters.""" + if not registered: + self.agent_name = "not-registered" + + if performative == TendermintMessage.Performative.GET_RECOVERY_PARAMS: + self.context.state.tm_recovery_params = TendermintRecoveryParams( + "DummyRound" + ) + message = TendermintMessage(performative) # type: ignore + log_message = self.handler.LogMessages.sending_request_response.value + elif performative == TendermintMessage.Performative.RECOVERY_PARAMS: + params = json.dumps( + asdict(TendermintRecoveryParams(DummyRound.auto_round_id())) + ) + message = TendermintMessage(performative, params=params) # type: ignore + log_message = self.handler.LogMessages.collected_params.value + else: + raise AssertionError( + f"Invalid performative {performative} for `test_recovery_params`." + ) + + self.context.agent_address = message.sender = self.agent_name + tm_configs = {self.agent_name: {"dummy": "value"}} if registered else {} + + self.handler.initial_tm_configs = tm_configs + self.handler.handle(message) + + if not registered: + log_message = self.handler.LogMessages.not_in_registered_addresses.value + + assert log_message in caplog.text + + @pytest.mark.parametrize( + "side_effect, expected_exception", + ( + ( + json.decoder.JSONDecodeError("", "", 0), + ": line 1 column 1 (char 0)", + ), + ( + {"not a dict"}, + "argument after ** must be a mapping, not str", + ), + ), + ) + def test_recovery_params_error( + self, + side_effect: Any, + expected_exception: str, + caplog: LogCaptureFixture, + ) -> None: + """Test handle response for recovery parameters.""" + message = TendermintMessage( + TendermintMessage.Performative.RECOVERY_PARAMS, params=MagicMock() # type: ignore + ) + + self.context.agent_address = message.sender = self.agent_name + tm_configs = {self.agent_name: {"dummy": "value"}} + self.handler.initial_tm_configs = tm_configs + with mock.patch.object(json, "loads", side_effect=side_effect): + self.handler.handle(message) + + log_message = self.handler.LogMessages.failed_to_parse_params.value + assert log_message in caplog.text + assert expected_exception in caplog.text + + # error + def test_handle_error(self, caplog: LogCaptureFixture) -> None: + """Test handle error""" + message = self.make_error_message() + self.handler.initial_tm_configs = self.dummy_validator_config + self.handler.handle(message) + log_message = self.handler.LogMessages.received_error_response.value + assert log_message in caplog.text + + def test_handle_error_no_target_message_retrieved( + self, caplog: LogCaptureFixture + ) -> None: + """Test handle error no target message retrieved""" + message, nonce = self.make_error_message(), "0" + dialogue = TendermintDialogue(mock.Mock(), "Bob", mock.Mock()) + dialogue.dialogue_label.dialogue_reference = nonce, "stub" + self.handler.dialogues.update = lambda _: dialogue # type: ignore + callback = lambda *args, **kwargs: None # noqa: E731 + self.context.requests.request_id_to_callback = {nonce: callback} + self.handler.initial_tm_configs = self.dummy_validator_config + self.handler.handle(message) + log_message = ( + self.handler.LogMessages.received_error_without_target_message.value + ) + assert log_message in caplog.text + + # performative + def test_handle_performative_not_recognized( + self, caplog: LogCaptureFixture + ) -> None: + """Test performative no recognized""" + message = self.make_error_message() + message._slots.performative = MagicMock(value="wacky") + self.handler.initial_tm_configs = self.dummy_validator_config + self.handler.handle(message) + log_message = self.handler.LogMessages.performative_not_recognized.value + assert log_message in caplog.text + + def test_sender_not_in_registered_addresses( + self, caplog: LogCaptureFixture + ) -> None: + """Test sender not in registered addresses.""" + + performative = TendermintMessage.Performative.GET_GENESIS_INFO + message = TendermintMessage(performative) # type: ignore + self.context.agent_address = message.sender = "dummy" + self.handler.initial_tm_configs = self.dummy_validator_config + self.handler.handle(message) + log_message = self.handler.LogMessages.not_in_registered_addresses.value + assert log_message in caplog.text diff --git a/packages/valory/skills/abstract_round_abci/tests/test_io/__init__.py b/packages/valory/skills/abstract_round_abci/tests/test_io/__init__.py new file mode 100644 index 0000000..b640bcf --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/test_io/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Package for `io` testing.""" diff --git a/packages/valory/skills/abstract_round_abci/tests/test_io/test_ipfs.py b/packages/valory/skills/abstract_round_abci/tests/test_io/test_ipfs.py new file mode 100644 index 0000000..ce81b61 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/test_io/test_ipfs.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains tests for the `IPFS` interactions.""" + +# pylint: skip-file + +import os +from pathlib import PosixPath +from typing import Any, Dict, cast +from unittest import mock + +import pytest + +from packages.valory.skills.abstract_round_abci.io_.ipfs import ( + IPFSInteract, + IPFSInteractionError, +) +from packages.valory.skills.abstract_round_abci.io_.load import AbstractLoader +from packages.valory.skills.abstract_round_abci.io_.store import ( + AbstractStorer, + StoredJSONType, + SupportedFiletype, +) + + +use_ipfs_daemon = pytest.mark.usefixtures("ipfs_daemon") + + +class TestIPFSInteract: + """Test `IPFSInteract`.""" + + def setup(self) -> None: + """Setup test class.""" + self.ipfs_interact = IPFSInteract() + + @pytest.mark.parametrize("multiple", (True, False)) + def test_store_and_send_and_back( + self, + multiple: bool, + dummy_obj: StoredJSONType, + dummy_multiple_obj: Dict[str, StoredJSONType], + tmp_path: PosixPath, + ) -> None: + """Test store -> send -> download -> read of objects.""" + obj: StoredJSONType + if multiple: + obj = dummy_multiple_obj + filepath = "dummy_dir" + else: + obj = dummy_obj + filepath = "test_file.json" + + filepath = str(tmp_path / filepath) + serialized_objects = self.ipfs_interact.store( + filepath, obj, multiple, SupportedFiletype.JSON + ) + expected_objects = obj + actual_objects = cast( + Dict[str, Any], + self.ipfs_interact.load( + serialized_objects, + SupportedFiletype.JSON, + ), + ) + if multiple: + # here we manually remove the trailing the dir from the name. + # This is done by the IPFS connection under normal circumstances. + actual_objects = {os.path.basename(k): v for k, v in actual_objects.items()} + + assert actual_objects == expected_objects + + def test_store_fails(self, dummy_multiple_obj: Dict[str, StoredJSONType]) -> None: + """Tests when "store" fails.""" + dummy_filepath = "dummy_dir" + multiple = False + with mock.patch.object( + AbstractStorer, + "store", + side_effect=ValueError, + ), pytest.raises(IPFSInteractionError): + self.ipfs_interact.store( + dummy_filepath, dummy_multiple_obj, multiple, SupportedFiletype.JSON + ) + + def test_load_fails(self, dummy_multiple_obj: Dict[str, StoredJSONType]) -> None: + """Tests when "load" fails.""" + dummy_object = {"test": "test"} + with mock.patch.object( + AbstractLoader, + "load", + side_effect=ValueError, + ), pytest.raises(IPFSInteractionError): + self.ipfs_interact.load(dummy_object) diff --git a/packages/valory/skills/abstract_round_abci/tests/test_io/test_load.py b/packages/valory/skills/abstract_round_abci/tests/test_io/test_load.py new file mode 100644 index 0000000..ae48cef --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/test_io/test_load.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Tests for the loading functionality of abstract round abci.""" + +# pylint: skip-file + +import json +from typing import Optional, cast + +import pytest + +from packages.valory.skills.abstract_round_abci.io_.load import ( + CustomLoaderType, + JSONLoader, + Loader, + SupportedLoaderType, +) +from packages.valory.skills.abstract_round_abci.io_.store import SupportedFiletype + + +class TestLoader: + """Tests for the `Loader`.""" + + def setup(self) -> None: + """Setup the tests.""" + self.json_loader = Loader(SupportedFiletype.JSON, None) + + def __dummy_custom_loader(self) -> None: + """A dummy custom loading function to use for the tests.""" + + @staticmethod + @pytest.mark.parametrize( + "filetype, custom_loader, expected_loader", + ( + (None, None, None), + (SupportedFiletype.JSON, None, JSONLoader.load_single_object), + ( + SupportedFiletype.JSON, + __dummy_custom_loader, + JSONLoader.load_single_object, + ), + (None, __dummy_custom_loader, __dummy_custom_loader), + ), + ) + def test__get_loader_from_filetype( + filetype: Optional[SupportedFiletype], + custom_loader: CustomLoaderType, + expected_loader: Optional[SupportedLoaderType], + ) -> None: + """Test `_get_loader_from_filetype`.""" + if all( + test_arg is None for test_arg in (filetype, custom_loader, expected_loader) + ): + with pytest.raises( + ValueError, + match="Please provide either a supported filetype or a custom loader function.", + ): + Loader(filetype, custom_loader)._get_single_loader_from_filetype() + + else: + expected_loader = cast(SupportedLoaderType, expected_loader) + loader = Loader(filetype, custom_loader) + assert ( + loader._get_single_loader_from_filetype().__code__.co_code + == expected_loader.__code__.co_code + ) + + def test_load(self) -> None: + """Test `load`.""" + expected_object = dummy_object = {"test": "test"} + filename = "test" + serialized_object = json.dumps(dummy_object) + actual_object = self.json_loader.load({filename: serialized_object}) + assert expected_object == actual_object + + def test_no_object(self) -> None: + """Test `load` throws error when no object is provided.""" + with pytest.raises( + ValueError, + match='"serialized_objects" does not contain any objects', + ): + self.json_loader.load({}) + + def test_load_multiple_objects(self) -> None: + """Test `load` when multiple objects are to be deserialized.""" + dummy_object = {"test": "test"} + serialized_object = json.dumps(dummy_object) + serialized_objects = { + "obj1": serialized_object, + "obj2": serialized_object, + } + expected_objects = { + "obj1": dummy_object, + "obj2": dummy_object, + } + actual_objects = self.json_loader.load(serialized_objects) + assert expected_objects == actual_objects diff --git a/packages/valory/skills/abstract_round_abci/tests/test_io/test_store.py b/packages/valory/skills/abstract_round_abci/tests/test_io/test_store.py new file mode 100644 index 0000000..eae3d53 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/test_io/test_store.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Tests for the storing functionality of abstract round abci.""" + +# pylint: skip-file + +import json +from pathlib import Path, PosixPath +from typing import Optional, cast + +import pytest + +from packages.valory.skills.abstract_round_abci.io_.store import ( + CustomStorerType, + JSONStorer, + Storer, + SupportedFiletype, + SupportedStorerType, +) + + +class TestStorer: + """Tests for the `Storer`.""" + + def setup(self) -> None: + """Setup the tests.""" + self.path = "tmp" + self.json_storer = Storer(SupportedFiletype.JSON, None, self.path) + + def __dummy_custom_storer(self) -> None: + """A dummy custom storing function to use for the tests.""" + + @staticmethod + @pytest.mark.parametrize( + "filetype, custom_storer, expected_storer", + ( + (None, None, None), + (SupportedFiletype.JSON, None, JSONStorer.serialize_object), + ( + SupportedFiletype.JSON, + __dummy_custom_storer, + JSONStorer.serialize_object, + ), + (None, __dummy_custom_storer, __dummy_custom_storer), + ), + ) + def test__get_single_storer_from_filetype( + filetype: Optional[SupportedFiletype], + custom_storer: Optional[CustomStorerType], + expected_storer: Optional[SupportedStorerType], + tmp_path: PosixPath, + ) -> None: + """Test `_get_single_storer_from_filetype`.""" + if all( + test_arg is None for test_arg in (filetype, custom_storer, expected_storer) + ): + with pytest.raises( + ValueError, + match="Please provide either a supported filetype or a custom storing function.", + ): + Storer( + filetype, custom_storer, str(tmp_path) + )._get_single_storer_from_filetype() + + else: + expected_storer = cast(SupportedStorerType, expected_storer) + storer = Storer(filetype, custom_storer, str(tmp_path)) + assert ( + storer._get_single_storer_from_filetype().__code__.co_code + == expected_storer.__code__.co_code + ) + + def test_store(self) -> None: + """Test `store`.""" + dummy_object = {"test": "test"} + expected_object = {self.path: json.dumps(dummy_object, indent=4)} + actual_object = self.json_storer.store(dummy_object, False) + assert expected_object == actual_object + + def test_store_multiple(self) -> None: + """Test `store` when multiple files are present.""" + dummy_object = {"test": "test"} + dummy_filename = "test" + expected_path = Path(f"{self.path}/{dummy_filename}").__str__() + expected_object = {expected_path: json.dumps(dummy_object, indent=4)} + actual_object = self.json_storer.store({dummy_filename: dummy_object}, True) + assert expected_object == actual_object diff --git a/packages/valory/skills/abstract_round_abci/tests/test_models.py b/packages/valory/skills/abstract_round_abci/tests/test_models.py new file mode 100644 index 0000000..ee598b7 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/test_models.py @@ -0,0 +1,901 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test the models.py module of the skill.""" + +# pylint: skip-file + +import builtins +import json +import logging +import re +from collections import OrderedDict +from dataclasses import dataclass +from enum import Enum +from pathlib import Path +from tempfile import TemporaryDirectory +from time import sleep +from typing import Any, Dict, List, Optional, Set, Tuple, Type, cast +from unittest import mock +from unittest.mock import MagicMock + +import pytest +from aea.exceptions import AEAEnforceError +from typing_extensions import Literal, TypedDict + +from packages.valory.skills.abstract_round_abci.base import ( + AbstractRound, + BaseSynchronizedData, + OffenceStatus, + OffenseStatusEncoder, + ROUND_COUNT_DEFAULT, +) +from packages.valory.skills.abstract_round_abci.models import ( + ApiSpecs, + BaseParams, + BenchmarkTool, + DEFAULT_BACKOFF_FACTOR, + GenesisBlock, + GenesisConfig, + GenesisConsensusParams, + GenesisEvidence, + GenesisValidator, + MIN_RESET_PAUSE_DURATION, + NUMBER_OF_RETRIES, + Requests, +) +from packages.valory.skills.abstract_round_abci.models import ( + SharedState as BaseSharedState, +) +from packages.valory.skills.abstract_round_abci.models import ( + TendermintRecoveryParams, + _MetaSharedState, + check_type, +) +from packages.valory.skills.abstract_round_abci.test_tools.abci_app import AbciAppTest +from packages.valory.skills.abstract_round_abci.tests.conftest import ( + irrelevant_genesis_config, +) + + +BASE_DUMMY_SPECS_CONFIG = dict( + name="dummy", + skill_context=MagicMock(), + url="http://dummy", + api_id="api_id", + method="GET", + headers=OrderedDict([("Dummy-Header", "dummy_value")]), + parameters=OrderedDict([("Dummy-Param", "dummy_param")]), +) + +BASE_DUMMY_PARAMS = dict( + name="", + skill_context=MagicMock(is_abstract_component=True), + setup={}, + tendermint_url="", + max_healthcheck=1, + round_timeout_seconds=1.0, + sleep_time=1, + retry_timeout=1, + retry_attempts=1, + reset_pause_duration=MIN_RESET_PAUSE_DURATION, + drand_public_key="", + tendermint_com_url="", + reset_tendermint_after=1, + service_id="abstract_round_abci", + service_registry_address="0xa51c1fc2f0d1a1b8494ed1fe312d7c3a78ed91c0", + keeper_timeout=1.0, + tendermint_check_sleep_delay=3, + tendermint_max_retries=5, + cleanup_history_depth=0, + genesis_config=irrelevant_genesis_config, + cleanup_history_depth_current=None, + request_timeout=0.0, + request_retry_delay=0.0, + tx_timeout=0.0, + max_attempts=0, + on_chain_service_id=None, + share_tm_config_on_startup=False, + tendermint_p2p_url="", + use_termination=False, + use_slashing=False, + slash_cooldown_hours=3, + slash_threshold_amount=10_000_000_000_000_000, + light_slash_unit_amount=5_000_000_000_000_000, + serious_slash_unit_amount=8_000_000_000_000_000, +) + + +class TestApiSpecsModel: + """Test ApiSpecsModel.""" + + api_specs: ApiSpecs + + def setup( + self, + ) -> None: + """Setup test.""" + + self.api_specs = ApiSpecs( + **BASE_DUMMY_SPECS_CONFIG, + response_key="value", + response_index=0, + response_type="float", + error_key="error", + error_index=None, + error_type="str", + error_data="error text", + ) + + def test_init( + self, + ) -> None: + """Test initialization.""" + + # test ensure method. + with pytest.raises( + AEAEnforceError, + match="'url' of type '' required, but it is not set in `models.params.args` of `skill.yaml` of", + ): + _ = ApiSpecs( + name="dummy", + skill_context=MagicMock(), + ) + + assert self.api_specs.retries_info.backoff_factor == DEFAULT_BACKOFF_FACTOR + assert self.api_specs.retries_info.retries == NUMBER_OF_RETRIES + assert self.api_specs.retries_info.retries_attempted == 0 + + assert self.api_specs.url == "http://dummy" + assert self.api_specs.api_id == "api_id" + assert self.api_specs.method == "GET" + assert self.api_specs.headers == {"Dummy-Header": "dummy_value"} + assert self.api_specs.parameters == {"Dummy-Param": "dummy_param"} + assert self.api_specs.response_info.response_key == "value" + assert self.api_specs.response_info.response_index == 0 + assert self.api_specs.response_info.response_type == "float" + assert self.api_specs.response_info.error_key == "error" + assert self.api_specs.response_info.error_index is None + assert self.api_specs.response_info.error_type == "str" + assert self.api_specs.response_info.error_data is None + + @pytest.mark.parametrize("retries", range(10)) + def test_suggested_sleep_time(self, retries: int) -> None: + """Test `suggested_sleep_time`""" + self.api_specs.retries_info.retries_attempted = retries + assert ( + self.api_specs.retries_info.suggested_sleep_time + == DEFAULT_BACKOFF_FACTOR**retries + ) + + def test_retries( + self, + ) -> None: + """Tests for retries.""" + + self.api_specs.increment_retries() + assert self.api_specs.retries_info.retries_attempted == 1 + assert not self.api_specs.is_retries_exceeded() + + for _ in range(NUMBER_OF_RETRIES): + self.api_specs.increment_retries() + assert self.api_specs.is_retries_exceeded() + self.api_specs.reset_retries() + assert self.api_specs.retries_info.retries_attempted == 0 + + def test_get_spec( + self, + ) -> None: + """Test get_spec method.""" + + actual_specs = { + "url": "http://dummy", + "method": "GET", + "headers": {"Dummy-Header": "dummy_value"}, + "parameters": {"Dummy-Param": "dummy_param"}, + } + + specs = self.api_specs.get_spec() + assert all([key in specs for key in actual_specs.keys()]) + assert all([specs[key] == actual_specs[key] for key in actual_specs]) + + @pytest.mark.parametrize( + "api_specs_config, message, expected_res, expected_error", + ( + ( + dict( + **BASE_DUMMY_SPECS_CONFIG, + response_key="value", + response_index=None, + response_type="float", + error_key=None, + error_index=None, + error_data=None, + ), + MagicMock(body=b'{"value": "10.232"}'), + 10.232, + None, + ), + ( + dict( + **BASE_DUMMY_SPECS_CONFIG, + response_key="test:response:key", + response_index=2, + response_type="dict", + error_key="error:key", + error_index=3, + error_type="str", + error_data=None, + ), + MagicMock( + body=b'{"test": {"response": {"key": ["does_not_matter", "does_not_matter", {"this": "matters"}]}}}' + ), + {"this": "matters"}, + None, + ), + ( + dict( + **BASE_DUMMY_SPECS_CONFIG, + response_key="test:response:key", + response_index=2, + error_key="error:key", + error_index=3, + error_type="str", + error_data=None, + ), + MagicMock(body=b'{"cannot be parsed'), + None, + None, + ), + ( + dict( + **BASE_DUMMY_SPECS_CONFIG, + response_key="test:response:key", + response_index=2, + error_key="error:key", + error_index=3, + error_type="str", + error_data=None, + ), + MagicMock( + # the null will raise `TypeError` and we test that it is handled + body=b'{"test": {"response": {"key": ["does_not_matter", "does_not_matter", null]}}}' + ), + "None", + None, + ), + ( + dict( + **BASE_DUMMY_SPECS_CONFIG, + response_key="test:response:key", + response_index=2, # this will raise `IndexError` and we test that it is handled + error_key="error:key", + error_index=3, + error_type="str", + error_data=None, + ), + MagicMock( + body=b'{"test": {"response": {"key": ["does_not_matter", "does_not_matter"]}}}' + ), + None, + None, + ), + ( + dict( + **BASE_DUMMY_SPECS_CONFIG, + response_key="test:response:key", # this will raise `KeyError` and we test that it is handled + response_index=2, + error_key="error:key", + error_index=3, + error_type="str", + error_data=None, + ), + MagicMock( + body=b'{"test": {"response": {"key_does_not_match": ["does_not_matter", "does_not_matter"]}}}' + ), + None, + None, + ), + ( + dict( + **BASE_DUMMY_SPECS_CONFIG, + response_key="test:response:key", + response_index=2, + error_key="error:key", + error_index=3, + error_type="str", + error_data=None, + ), + MagicMock( + body=b'{"test": {"response": {"key_does_not_match": ["does_not_matter", "does_not_matter"]}}, ' + b'"error": {"key": [0, 1, 2, "test that the error is being parsed correctly"]}}' + ), + None, + "test that the error is being parsed correctly", + ), + ), + ) + def test_process_response( + self, + api_specs_config: dict, + message: MagicMock, + expected_res: Any, + expected_error: Any, + ) -> None: + """Test `process_response` method.""" + api_specs = ApiSpecs(**api_specs_config) + actual = api_specs.process_response(message) + assert actual == expected_res + response_type = api_specs_config.get("response_type", None) + if response_type is not None: + assert type(actual) == getattr(builtins, response_type) + assert api_specs.response_info.error_data == expected_error + + def test_attribute_manipulation(self) -> None: + """Test manipulating the attributes.""" + with pytest.raises(AttributeError, match="This object is frozen!"): + del self.api_specs.url + + with pytest.raises(AttributeError, match="This object is frozen!"): + self.api_specs.url = "" + + self.api_specs.__dict__["_frozen"] = False + self.api_specs.url = "" + del self.api_specs.url + + +class ConcreteRound(AbstractRound): + """A ConcreteRoundA for testing purposes.""" + + synchronized_data_class = MagicMock() + payload_attribute = MagicMock() + payload_class = MagicMock() + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: + """Handle the end of the block.""" + + +class SharedState(BaseSharedState): + """Shared State for testing purposes.""" + + abci_app_cls = AbciAppTest + + +class TestSharedState: + """Test SharedState(Model) class.""" + + def test_initialization(self, *_: Any) -> None: + """Test the initialization of the shared state.""" + SharedState(name="", skill_context=MagicMock()) + + @staticmethod + def dummy_state_setup(shared_state: SharedState) -> None: + """Setup a shared state instance with dummy params.""" + shared_state.context.params.setup_params = { + "test": [], + "all_participants": list(range(4)), + } + shared_state.setup() + + @pytest.mark.parametrize( + "acn_configured_agents, validator_to_agent, raises", + ( + ( + {i for i in range(4)}, + {f"validator_address_{i}": i for i in range(4)}, + False, + ), + ( + {i for i in range(5)}, + {f"validator_address_{i}": i for i in range(4)}, + True, + ), + ( + {i for i in range(4)}, + {f"validator_address_{i}": i for i in range(5)}, + True, + ), + ), + ) + def test_setup_slashing( + self, + acn_configured_agents: Set[str], + validator_to_agent: Dict[str, str], + raises: bool, + ) -> None: + """Test the `validator_to_agent` properties.""" + shared_state = SharedState(name="", skill_context=MagicMock()) + self.dummy_state_setup(shared_state) + + if not raises: + shared_state.initial_tm_configs = dict.fromkeys(acn_configured_agents) + shared_state.setup_slashing(validator_to_agent) + assert shared_state.round_sequence.validator_to_agent == validator_to_agent + + status = shared_state.round_sequence.offence_status + encoded_status = json.dumps( + status, + cls=OffenseStatusEncoder, + ) + expected_status = { + agent: OffenceStatus() for agent in acn_configured_agents + } + encoded_expected_status = json.dumps( + expected_status, cls=OffenseStatusEncoder + ) + + assert encoded_status == encoded_expected_status + + random_agent = acn_configured_agents.pop() + status[random_agent].num_unknown_offenses = 10 + assert status[random_agent].num_unknown_offenses == 10 + + for other_agent in acn_configured_agents - {random_agent}: + assert status[other_agent].num_unknown_offenses == 0 + + return + + expected_diff = acn_configured_agents.symmetric_difference( + validator_to_agent.values() + ) + with pytest.raises( + ValueError, + match=re.escape( + f"Trying to use the mapping `{validator_to_agent}`, which contains validators for non-configured " + "agents and/or does not contain validators for some configured agents. The agents which have been " + f"configured via ACN are `{acn_configured_agents}` and the diff was for {expected_diff}." + ), + ): + shared_state.initial_tm_configs = dict.fromkeys(acn_configured_agents) + shared_state.setup_slashing(validator_to_agent) + + def test_setup(self, *_: Any) -> None: + """Test setup method.""" + shared_state = SharedState( + name="", skill_context=MagicMock(is_abstract_component=False) + ) + assert shared_state.initial_tm_configs == {} + self.dummy_state_setup(shared_state) + assert shared_state.initial_tm_configs == {i: None for i in range(4)} + + @pytest.mark.parametrize( + "initial_tm_configs, address_input, exception, expected", + ( + ( + {}, + "0x1", + "The validator address of non-participating agent `0x1` was requested.", + None, + ), + ({}, "0x0", "SharedState's setup was not performed successfully.", None), + ( + {"0x0": None}, + "0x0", + "ACN registration has not been successfully performed for agent `0x0`. " + "Have you set the `share_tm_config_on_startup` flag to `true` in the configuration?", + None, + ), + ( + {"0x0": {}}, + "0x0", + "The tendermint configuration for agent `0x0` is invalid: `{}`.", + None, + ), + ( + {"0x0": {"address": None}}, + "0x0", + "The tendermint configuration for agent `0x0` is invalid: `{'address': None}`.", + None, + ), + ( + {"0x0": {"address": "test_validator_address"}}, + "0x0", + None, + "test_validator_address", + ), + ), + ) + def test_get_validator_address( + self, + initial_tm_configs: Dict[str, Optional[Dict[str, Any]]], + address_input: str, + exception: Optional[str], + expected: Optional[str], + ) -> None: + """Test `get_validator_address` method.""" + shared_state = SharedState(name="", skill_context=MagicMock()) + with mock.patch.object(shared_state.context, "params") as mock_params: + mock_params.setup_params = { + "all_participants": ["0x0"], + } + shared_state.setup() + shared_state.initial_tm_configs = initial_tm_configs + if exception is None: + assert shared_state.get_validator_address(address_input) == expected + return + with pytest.raises(ValueError, match=exception): + shared_state.get_validator_address(address_input) + + @pytest.mark.parametrize("self_idx", (range(4))) + def test_acn_container(self, self_idx: int) -> None: + """Test the `acn_container` method.""" + + shared_state = SharedState( + name="", skill_context=MagicMock(agent_address=self_idx) + ) + self.dummy_state_setup(shared_state) + expected = {i: None for i in range(4) if i != self_idx} + assert shared_state.acn_container() == expected + + def test_synchronized_data_negative_not_available(self, *_: Any) -> None: + """Test 'synchronized_data' property getter, negative case (not available).""" + shared_state = SharedState(name="", skill_context=MagicMock()) + with pytest.raises(ValueError, match="round sequence not available"): + shared_state.synchronized_data + + def test_synchronized_data_positive(self, *_: Any) -> None: + """Test 'synchronized_data' property getter, negative case (not available).""" + shared_state = SharedState(name="", skill_context=MagicMock()) + shared_state.context.params.setup_params = { + "test": [], + "all_participants": [["0x0"]], + } + shared_state.setup() + shared_state.round_sequence.abci_app._round_results = [MagicMock()] + shared_state.synchronized_data + + def test_synchronized_data_db(self, *_: Any) -> None: + """Test 'synchronized_data' AbciAppDB.""" + shared_state = SharedState(name="", skill_context=MagicMock()) + with mock.patch.object(shared_state.context, "params") as mock_params: + mock_params.setup_params = { + "safe_contract_address": "0xsafe", + "oracle_contract_address": "0xoracle", + "all_participants": "0x0", + } + shared_state.setup() + for key, value in mock_params.setup_params.items(): + assert shared_state.synchronized_data.db.get_strict(key) == value + + @pytest.mark.parametrize( + "address_to_acn_deliverable, n_participants, expected", + ( + ({}, 4, None), + ({i: "test" for i in range(4)}, 4, "test"), + ( + {i: TendermintRecoveryParams("test") for i in range(4)}, + 4, + TendermintRecoveryParams("test"), + ), + ({1: "test", 2: "non-matching", 3: "test", 4: "test"}, 4, "test"), + ({i: "test" for i in range(4)}, 4, "test"), + ({1: "no", 2: "result", 3: "matches", 4: ""}, 4, None), + ), + ) + def test_get_acn_result( + self, + address_to_acn_deliverable: Dict[str, Any], + n_participants: int, + expected: Optional[str], + ) -> None: + """Test `get_acn_result`.""" + shared_state = SharedState( + abci_app_cls=AbciAppTest, name="", skill_context=MagicMock() + ) + shared_state.context.params.setup_params = { + "test": [], + "all_participants": ["0x0"], + } + shared_state.setup() + shared_state.synchronized_data.update(participants=tuple(range(n_participants))) + shared_state.address_to_acn_deliverable = address_to_acn_deliverable + actual = shared_state.get_acn_result() + + assert actual == expected + + def test_recovery_params_on_init(self) -> None: + """Test that `tm_recovery_params` get initialized correctly.""" + shared_state = SharedState(name="", skill_context=MagicMock()) + assert shared_state.tm_recovery_params is not None + assert shared_state.tm_recovery_params.round_count == ROUND_COUNT_DEFAULT + assert ( + shared_state.tm_recovery_params.reset_from_round + == AbciAppTest.initial_round_cls.auto_round_id() + ) + assert shared_state.tm_recovery_params.reset_params is None + + def test_set_last_reset_params(self) -> None: + """Test that `last_reset_params` get set correctly.""" + shared_state = SharedState(name="", skill_context=MagicMock()) + test_params = [("genesis_time", "some-time"), ("initial_height", "0")] + shared_state.last_reset_params = test_params + assert shared_state.last_reset_params == test_params + + +class TestBenchmarkTool: + """Test BenchmarkTool""" + + @staticmethod + def _check_behaviour_data(data: List, agent_name: str) -> None: + """Check behaviour data.""" + assert len(data) == 1 + + (behaviour_data,) = data + assert behaviour_data["behaviour"] == agent_name + assert all( + [key in behaviour_data["data"] for key in ("local", "consensus", "total")] + ) + + def test_end_2_end(self) -> None: + """Test end 2 end of the tool.""" + + agent_name = "agent" + skill_context = MagicMock( + agent_address=agent_name, logger=MagicMock(info=logging.info) + ) + + with TemporaryDirectory() as temp_dir: + benchmark = BenchmarkTool( + name=agent_name, skill_context=skill_context, log_dir=temp_dir + ) + + with benchmark.measure(agent_name).local(): + sleep(1.0) + + with benchmark.measure(agent_name).consensus(): + sleep(1.0) + + self._check_behaviour_data(benchmark.data, agent_name) + + benchmark.save() + + benchmark_dir = Path(temp_dir, agent_name) + benchmark_file = benchmark_dir / "0.json" + assert (benchmark_file).is_file() + + behaviour_data = json.loads(benchmark_file.read_text()) + self._check_behaviour_data(behaviour_data, agent_name) + + +def test_requests_model_initialization() -> None: + """Test initialization of the 'Requests(Model)' class.""" + Requests(name="", skill_context=MagicMock()) + + +def test_base_params_model_initialization() -> None: + """Test initialization of the 'BaseParams(Model)' class.""" + kwargs = BASE_DUMMY_PARAMS.copy() + bp = BaseParams(**kwargs) + + with pytest.raises(AttributeError, match="This object is frozen!"): + bp.request_timeout = 0.1 + + with pytest.raises(AttributeError, match="This object is frozen!"): + del bp.request_timeout + + bp.__dict__["_frozen"] = False + del bp.request_timeout + + assert getattr(bp, "request_timeout", None) is None + + kwargs["skill_context"] = MagicMock(is_abstract_component=False) + required_setup_params = { + "safe_contract_address": "0x0", + "all_participants": ["0x0"], + "consensus_threshold": 1, + } + kwargs["setup"] = required_setup_params + BaseParams(**kwargs) + + +@pytest.mark.parametrize( + "setup, error_text", + ( + ({}, "`setup` params contain no values!"), + ( + {"a": "b"}, + "Value for `safe_contract_address` missing from the `setup` params.", + ), + ), +) +def test_incorrect_setup(setup: Dict[str, Any], error_text: str) -> None: + """Test BaseParams model initialization with incorrect setup data.""" + kwargs = BASE_DUMMY_PARAMS.copy() + + with pytest.raises( + AEAEnforceError, + match=error_text, + ): + kwargs["skill_context"] = MagicMock(is_abstract_component=False) + kwargs["setup"] = setup + BaseParams(**kwargs) + + with pytest.raises( + AEAEnforceError, + match=f"`reset_pause_duration` must be greater than or equal to {MIN_RESET_PAUSE_DURATION}", + ): + kwargs["reset_pause_duration"] = MIN_RESET_PAUSE_DURATION - 1 + BaseParams(**kwargs) + + +def test_genesis_block() -> None: + """Test genesis block methods.""" + json = {"max_bytes": "a", "max_gas": "b", "time_iota_ms": "c"} + gb = GenesisBlock(**json) + assert gb.to_json() == json + + with pytest.raises(TypeError, match="Error in field 'max_bytes'. Expected type .*"): + json["max_bytes"] = 0 # type: ignore + GenesisBlock(**json) + + +def test_genesis_evidence() -> None: + """Test genesis evidence methods.""" + json = {"max_age_num_blocks": "a", "max_age_duration": "b", "max_bytes": "c"} + ge = GenesisEvidence(**json) + assert ge.to_json() == json + + +def test_genesis_validator() -> None: + """Test genesis validator methods.""" + json = {"pub_key_types": ["a", "b"]} + ge = GenesisValidator(pub_key_types=tuple(json["pub_key_types"])) + assert ge.to_json() == json + + with pytest.raises( + TypeError, match="Error in field 'pub_key_types'. Expected type .*" + ): + GenesisValidator(**json) # type: ignore + + +def test_genesis_consensus_params() -> None: + """Test genesis consensus params methods.""" + consensus_params = cast(Dict, irrelevant_genesis_config["consensus_params"]) + gcp = GenesisConsensusParams.from_json_dict(consensus_params) + assert gcp.to_json() == consensus_params + + +def test_genesis_config() -> None: + """Test genesis config methods.""" + gcp = GenesisConfig.from_json_dict(irrelevant_genesis_config) + assert gcp.to_json() == irrelevant_genesis_config + + +def test_meta_shared_state_when_instance_not_subclass_of_shared_state() -> None: + """Test instantiation of meta class when instance not a subclass of shared state.""" + + class MySharedState(metaclass=_MetaSharedState): + pass + + +def test_shared_state_instantiation_without_attributes_raises_error() -> None: + """Test that definition of concrete subclass of SharedState without attributes raises error.""" + with pytest.raises(AttributeError, match="'abci_app_cls' not set on .*"): + + class MySharedState(BaseSharedState): + pass + + with pytest.raises(AttributeError, match="The object `None` is not a class"): + + class MySharedStateB(BaseSharedState): + abci_app_cls = None # type: ignore + + with pytest.raises( + AttributeError, + match="The class is not an instance of packages.valory.skills.abstract_round_abci.base.AbciApp", + ): + + class MySharedStateC(BaseSharedState): + abci_app_cls = MagicMock + + +@dataclass +class A: + """Class for testing.""" + + value: int + + +@dataclass +class B: + """Class for testing.""" + + value: str + + +class C(TypedDict): + """Class for testing.""" + + name: str + year: int + + +class D(TypedDict, total=False): + """Class for testing.""" + + name: str + year: int + + +testdata_positive = [ + ("test_arg", 1, int), + ("test_arg", "1", str), + ("test_arg", True, bool), + ("test_arg", 1, Optional[int]), + ("test_arg", None, Optional[int]), + ("test_arg", "1", Optional[str]), + ("test_arg", None, Optional[str]), + ("test_arg", None, Optional[bool]), + ("test_arg", None, Optional[List[int]]), + ("test_arg", [], Optional[List[int]]), + ("test_arg", [1], Optional[List[int]]), + ("test_arg", {"str": 1}, Optional[Dict[str, int]]), + ("test_arg", {"str": A(1)}, Dict[str, A]), + ("test_arg", [("1", "2")], List[Tuple[str, str]]), + ("test_arg", [1], List[Optional[int]]), + ("test_arg", [1, None], List[Optional[int]]), + ("test_arg", A, Type[A]), + ("test_arg", A, Optional[Type[A]]), + ("test_arg", None, Optional[Type[A]]), + ("test_arg", MagicMock(), Optional[Type[A]]), # any type allowed + ("test_arg", {"name": "str", "year": 1}, C), + ("test_arg", 42, Literal[42]), + ("test_arg", {"name": "str"}, D), +] + + +@pytest.mark.parametrize("name,value,type_hint", testdata_positive) +def test_type_check_positive(name: str, value: Any, type_hint: Any) -> None: + """Test the type check mixin.""" + + check_type(name, value, type_hint) + + +testdata_negative = [ + ("test_arg", "1", int), + ("test_arg", 1, str), + ("test_arg", None, bool), + ("test_arg", "1", Optional[int]), + ("test_arg", 1, Optional[str]), + ("test_arg", 1, Optional[bool]), + ("test_arg", ["1"], Optional[List[int]]), + ("test_arg", {"str": "1"}, Optional[Dict[str, int]]), + ("test_arg", {1: 1}, Optional[Dict[str, int]]), + ("test_arg", {"str": B("1")}, Dict[str, A]), + ("test_arg", [()], List[Tuple[str, str]]), + ("test_arg", [("1",)], List[Tuple[str, str]]), + ("test_arg", [("1", 1)], List[Tuple[str, str]]), + ("test_arg", [("1", 1, "1")], List[Tuple[str, ...]]), + ("test_arg", ["1"], List[Optional[int]]), + ("test_arg", [1, None, "1"], List[Optional[int]]), + ("test_arg", B, Type[A]), + ("test_arg", B, Optional[Type[A]]), + ("test_arg", {"name": "str", "year": "1"}, C), + ("test_arg", 41, Literal[42]), + ("test_arg", C({"name": "str", "year": 1}), A), + ("test_arg", {"name": "str"}, C), +] + + +@pytest.mark.parametrize("name,value,type_hint", testdata_negative) +def test_type_check_negative(name: str, value: Any, type_hint: Any) -> None: + """Test the type check mixin.""" + + with pytest.raises(TypeError): + check_type(name, value, type_hint) diff --git a/packages/valory/skills/abstract_round_abci/tests/test_tools/__init__.py b/packages/valory/skills/abstract_round_abci/tests/test_tools/__init__.py new file mode 100644 index 0000000..261d2d3 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/test_tools/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Package for `test_tools` testing.""" diff --git a/packages/valory/skills/abstract_round_abci/tests/test_tools/base.py b/packages/valory/skills/abstract_round_abci/tests/test_tools/base.py new file mode 100644 index 0000000..8595574 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/test_tools/base.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Tests for abstract_round_abci/test_tools/common.py""" + +from pathlib import Path +from typing import Any, Dict, Type, cast + +from aea.helpers.base import cd +from aea.test_tools.utils import copy_class + +from packages.valory.skills.abstract_round_abci.base import BaseTxPayload, _MetaPayload +from packages.valory.skills.abstract_round_abci.test_tools.base import ( + FSMBehaviourBaseCase, +) +from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci import ( + PATH_TO_SKILL, +) + + +class FSMBehaviourTestToolSetup: + """BaseRandomnessBehaviourTestSetup""" + + test_cls: Type[FSMBehaviourBaseCase] + __test_cls: Type[FSMBehaviourBaseCase] + __old_value: Dict[str, Type[BaseTxPayload]] + + @classmethod + def setup_class(cls) -> None: + """Setup class""" + + if not hasattr(cls, "test_cls"): + raise AttributeError(f"{cls} must set `test_cls`") + + cls.__test_cls = cls.test_cls + cls.__old_value = _MetaPayload.registry.copy() + _MetaPayload.registry.clear() + + @classmethod + def teardown_class(cls) -> None: + """Teardown class""" + _MetaPayload.registry = cls.__old_value + + def setup(self) -> None: + """Setup test""" + test_cls = copy_class(self.__test_cls) + self.test_cls = cast(Type[FSMBehaviourBaseCase], test_cls) + + def teardown(self) -> None: + """Teardown test""" + self.test_cls.teardown_class() + + def set_path_to_skill(self, path_to_skill: Path = PATH_TO_SKILL) -> None: + """Set path_to_skill""" + self.test_cls.path_to_skill = path_to_skill + + def setup_test_cls(self, **kwargs: Any) -> FSMBehaviourBaseCase: + """Helper method to setup test to be tested""" + + # different test tools will require the setting of + # different class attributes (such as path_to_skill). + # One should write a test that sets these, + # and subsequently invoke this method to test the setup. + + with cd(self.test_cls.path_to_skill): + self.test_cls.setup_class(**kwargs) + + test_instance = self.test_cls() + test_instance.setup() + return test_instance diff --git a/packages/valory/skills/abstract_round_abci/tests/test_tools/test_base.py b/packages/valory/skills/abstract_round_abci/tests/test_tools/test_base.py new file mode 100644 index 0000000..294c846 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/test_tools/test_base.py @@ -0,0 +1,189 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2022-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Tests for abstract_round_abci/test_tools/base.py""" + +from enum import Enum +from typing import Any, Dict, cast + +import pytest +from aea.mail.base import Envelope + +from packages.valory.connections.ledger.connection import ( + PUBLIC_ID as LEDGER_CONNECTION_PUBLIC_ID, +) +from packages.valory.protocols.contract_api.message import ContractApiMessage +from packages.valory.protocols.ledger_api.message import LedgerApiMessage +from packages.valory.skills.abstract_round_abci.base import AbciAppDB +from packages.valory.skills.abstract_round_abci.behaviours import BaseBehaviour +from packages.valory.skills.abstract_round_abci.test_tools.base import ( + DummyContext, + FSMBehaviourBaseCase, +) +from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci import PUBLIC_ID +from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci.behaviours import ( + DummyRoundBehaviour, +) +from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci.models import ( + SharedState, +) +from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci.rounds import ( + Event, + SynchronizedData, +) +from packages.valory.skills.abstract_round_abci.tests.test_tools.base import ( + FSMBehaviourTestToolSetup, +) + + +class TestFSMBehaviourBaseCaseSetup(FSMBehaviourTestToolSetup): + """test TestFSMBehaviourBaseCaseSetup setup""" + + test_cls = FSMBehaviourBaseCase + + @pytest.mark.parametrize("kwargs", [{}]) + def test_setup_fails_without_path(self, kwargs: Dict[str, Dict[str, Any]]) -> None: + """Test setup""" + with pytest.raises(ValueError): + self.test_cls.setup_class(**kwargs) + + @pytest.mark.parametrize("kwargs", [{}, {"param_overrides": {"new_p": None}}]) + def test_setup(self, kwargs: Dict[str, Dict[str, Any]]) -> None: + """Test setup""" + + self.set_path_to_skill() + test_instance = self.setup_test_cls(**kwargs) + assert test_instance + assert hasattr(test_instance.behaviour.context.params, "new_p") == bool(kwargs) + + @pytest.mark.parametrize("behaviour", DummyRoundBehaviour.behaviours) + def test_fast_forward_to_behaviour(self, behaviour: BaseBehaviour) -> None: + """Test fast_forward_to_behaviour""" + self.set_path_to_skill() + test_instance = self.setup_test_cls() + + skill = test_instance._skill # pylint: disable=protected-access + round_behaviour = skill.skill_context.behaviours.main + behaviour_id = behaviour.behaviour_id + synchronized_data = SynchronizedData( + AbciAppDB(setup_data=dict(participants=[tuple("abcd")])) + ) + + test_instance.fast_forward_to_behaviour( + behaviour=round_behaviour, + behaviour_id=behaviour_id, + synchronized_data=synchronized_data, + ) + + current_behaviour = test_instance.behaviour.current_behaviour + assert current_behaviour is not None + assert isinstance( + current_behaviour.synchronized_data, + SynchronizedData, + ) + assert current_behaviour.behaviour_id == behaviour.behaviour_id + assert ( # pylint: disable=protected-access + test_instance.skill.skill_context.state.round_sequence.abci_app._current_round_cls + == current_behaviour.matching_round + == behaviour.matching_round + ) + + @pytest.mark.parametrize("event", Event) + @pytest.mark.parametrize("set_none", [False, True]) + def test_end_round(self, event: Enum, set_none: bool) -> None: + """Test end_round""" + + self.set_path_to_skill() + test_instance = self.setup_test_cls() + current_behaviour = cast( + BaseBehaviour, test_instance.behaviour.current_behaviour + ) + abci_app = current_behaviour.context.state.round_sequence.abci_app + if set_none: + test_instance.behaviour.current_behaviour = None + assert abci_app.current_round_height == 0 + test_instance.end_round(event) + assert abci_app.current_round_height == 1 - int(set_none) + + def test_mock_ledger_api_request(self) -> None: + """Test mock_ledger_api_request""" + + self.set_path_to_skill() + test_instance = self.setup_test_cls() + + request_kwargs = dict(performative=LedgerApiMessage.Performative.GET_BALANCE) + response_kwargs = dict(performative=LedgerApiMessage.Performative.BALANCE) + with pytest.raises( + AssertionError, + match="Invalid number of messages in outbox. Expected 1. Found 0.", + ): + test_instance.mock_ledger_api_request(request_kwargs, response_kwargs) + + message = LedgerApiMessage(**request_kwargs, dialogue_reference=("a", "b")) # type: ignore + envelope = Envelope( + to=str(LEDGER_CONNECTION_PUBLIC_ID), + sender=str(PUBLIC_ID), + protocol_specification_id=LedgerApiMessage.protocol_specification_id, + message=message, + ) + multiplexer = test_instance._multiplexer # pylint: disable=protected-access + multiplexer.out_queue.put_nowait(envelope) + test_instance.mock_ledger_api_request(request_kwargs, response_kwargs) + + def test_mock_contract_api_request(self) -> None: + """Test mock_contract_api_request""" + + self.set_path_to_skill() + test_instance = self.setup_test_cls() + + contract_id = "dummy_contract" + request_kwargs = dict(performative=ContractApiMessage.Performative.GET_STATE) + response_kwargs = dict(performative=ContractApiMessage.Performative.STATE) + with pytest.raises( + AssertionError, + match="Invalid number of messages in outbox. Expected 1. Found 0.", + ): + test_instance.mock_contract_api_request( + contract_id, request_kwargs, response_kwargs + ) + + message = ContractApiMessage( + **request_kwargs, # type: ignore + dialogue_reference=("a", "b"), + ledger_id="ethereum", + contract_id=contract_id + ) + envelope = Envelope( + to=str(LEDGER_CONNECTION_PUBLIC_ID), + sender=str(PUBLIC_ID), + protocol_specification_id=ContractApiMessage.protocol_specification_id, + message=message, + ) + multiplexer = test_instance._multiplexer # pylint: disable=protected-access + multiplexer.out_queue.put_nowait(envelope) + test_instance.mock_contract_api_request( + contract_id, request_kwargs, response_kwargs + ) + + +def test_dummy_context_is_abstract_component() -> None: + """Test dummy context is abstract component""" + + shared_state = SharedState(name="dummy_shared_state", skill_context=DummyContext()) + assert shared_state.context.is_abstract_component diff --git a/packages/valory/skills/abstract_round_abci/tests/test_tools/test_common.py b/packages/valory/skills/abstract_round_abci/tests/test_tools/test_common.py new file mode 100644 index 0000000..b5cb5d2 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/test_tools/test_common.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2022-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Tests for abstract_round_abci/test_tools/common.py""" + +from typing import Type, Union, cast + +import pytest + +from packages.valory.skills.abstract_round_abci.test_tools.common import ( + BaseRandomnessBehaviourTest, + BaseSelectKeeperBehaviourTest, +) +from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci import ( + PATH_TO_SKILL, +) +from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci.behaviours import ( + DummyFinalBehaviour, + DummyKeeperSelectionBehaviour, + DummyRandomnessBehaviour, +) +from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci.rounds import ( + Event, +) +from packages.valory.skills.abstract_round_abci.tests.test_tools.base import ( + FSMBehaviourTestToolSetup, +) + + +class BaseCommonBaseCaseTestSetup(FSMBehaviourTestToolSetup): + """BaseRandomnessBehaviourTestSetup""" + + test_cls: Type[Union[BaseRandomnessBehaviourTest, BaseSelectKeeperBehaviourTest]] + + def set_done_event(self) -> None: + """Set done_event""" + self.test_cls.done_event = Event.DONE + + def set_next_behaviour_class(self, next_behaviour_class: Type) -> None: + """Set next_behaviour_class""" + self.test_cls.next_behaviour_class = next_behaviour_class + + +class TestBaseRandomnessBehaviourTestSetup(BaseCommonBaseCaseTestSetup): + """Test BaseRandomnessBehaviourTest setup.""" + + test_cls: Type[BaseRandomnessBehaviourTest] = BaseRandomnessBehaviourTest + + def set_randomness_behaviour_class(self) -> None: + """Set randomness_behaviour_class""" + self.test_cls.randomness_behaviour_class = DummyRandomnessBehaviour # type: ignore + + def test_setup_randomness_behaviour_class_not_set(self) -> None: + """Test setup randomness_behaviour_class not set.""" + + self.set_path_to_skill() + test_instance = cast(BaseRandomnessBehaviourTest, self.setup_test_cls()) + expected = f"'{self.test_cls.__name__}' object has no attribute 'randomness_behaviour_class'" + with pytest.raises(AttributeError, match=expected): + test_instance.test_randomness_behaviour() + + def test_setup_done_event_not_set(self) -> None: + """Test setup done_event = Event.DONE not set.""" + + self.set_path_to_skill() + self.set_randomness_behaviour_class() + + test_instance = cast(BaseRandomnessBehaviourTest, self.setup_test_cls()) + expected = f"'{self.test_cls.__name__}' object has no attribute 'done_event'" + with pytest.raises(AttributeError, match=expected): + test_instance.test_randomness_behaviour() + + def test_setup_next_behaviour_class_not_set(self) -> None: + """Test setup next_behaviour_class not set.""" + + self.set_path_to_skill() + self.set_randomness_behaviour_class() + self.set_done_event() + + test_instance = cast(BaseRandomnessBehaviourTest, self.setup_test_cls()) + expected = ( + f"'{self.test_cls.__name__}' object has no attribute 'next_behaviour_class'" + ) + with pytest.raises(AttributeError, match=expected): + test_instance.test_randomness_behaviour() + + def test_successful_setup_randomness_behaviour_test(self) -> None: + """Test successful setup of the test class inheriting from BaseRandomnessBehaviourTest.""" + + self.set_path_to_skill() + self.set_randomness_behaviour_class() + self.set_done_event() + self.set_next_behaviour_class(DummyKeeperSelectionBehaviour) + test_instance = cast(BaseRandomnessBehaviourTest, self.setup_test_cls()) + test_instance.test_randomness_behaviour() + + +class TestBaseRandomnessBehaviourTestRunning(BaseRandomnessBehaviourTest): + """Test TestBaseRandomnessBehaviourTestRunning running.""" + + path_to_skill = PATH_TO_SKILL + randomness_behaviour_class = DummyRandomnessBehaviour + next_behaviour_class = DummyKeeperSelectionBehaviour + done_event = Event.DONE + + +class TestBaseSelectKeeperBehaviourTestSetup(BaseCommonBaseCaseTestSetup): + """Test BaseRandomnessBehaviourTest setup.""" + + test_cls: Type[BaseSelectKeeperBehaviourTest] = BaseSelectKeeperBehaviourTest + + def set_select_keeper_behaviour_class(self) -> None: + """Set select_keeper_behaviour_class""" + self.test_cls.select_keeper_behaviour_class = DummyKeeperSelectionBehaviour # type: ignore + + def test_setup_select_keeper_behaviour_class_not_set(self) -> None: + """Test setup select_keeper_behaviour_class not set.""" + + self.set_path_to_skill() + test_instance = cast(BaseSelectKeeperBehaviourTest, self.setup_test_cls()) + expected = f"'{self.test_cls.__name__}' object has no attribute 'select_keeper_behaviour_class'" + with pytest.raises(AttributeError, match=expected): + test_instance.test_select_keeper_preexisting_keeper() + + def test_setup_done_event_not_set(self) -> None: + """Test setup done_event = Event.DONE not set.""" + + self.set_path_to_skill() + self.set_select_keeper_behaviour_class() + + test_instance = cast(BaseSelectKeeperBehaviourTest, self.setup_test_cls()) + expected = f"'{self.test_cls.__name__}' object has no attribute 'done_event'" + with pytest.raises(AttributeError, match=expected): + test_instance.test_select_keeper_preexisting_keeper() + + def test_setup_next_behaviour_class_not_set(self) -> None: + """Test setup next_behaviour_class not set.""" + + self.set_path_to_skill() + self.set_select_keeper_behaviour_class() + self.set_done_event() + + test_instance = cast(BaseSelectKeeperBehaviourTest, self.setup_test_cls()) + expected = ( + f"'{self.test_cls.__name__}' object has no attribute 'next_behaviour_class'" + ) + with pytest.raises(AttributeError, match=expected): + test_instance.test_select_keeper_preexisting_keeper() + + def test_successful_setup_select_keeper_behaviour_test(self) -> None: + """Test successful setup of the test class inheriting from BaseSelectKeeperBehaviourTest.""" + + self.set_path_to_skill() + self.set_select_keeper_behaviour_class() + self.set_done_event() + self.set_next_behaviour_class(DummyFinalBehaviour) + test_instance = cast(BaseSelectKeeperBehaviourTest, self.setup_test_cls()) + test_instance.test_select_keeper_preexisting_keeper() + + +class TestBaseSelectKeeperBehaviourTestRunning(BaseSelectKeeperBehaviourTest): + """Test BaseSelectKeeperBehaviourTest running.""" + + path_to_skill = PATH_TO_SKILL + select_keeper_behaviour_class = DummyKeeperSelectionBehaviour + next_behaviour_class = DummyFinalBehaviour + done_event = Event.DONE diff --git a/packages/valory/skills/abstract_round_abci/tests/test_tools/test_integration.py b/packages/valory/skills/abstract_round_abci/tests/test_tools/test_integration.py new file mode 100644 index 0000000..e71ed92 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/test_tools/test_integration.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2022-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Tests for abstract_round_abci/test_tools/integration.py""" + +from typing import cast + +import pytest + +from packages.open_aea.protocols.signing import SigningMessage +from packages.open_aea.protocols.signing.custom_types import SignedMessage +from packages.valory.connections.ledger.connection import ( + PUBLIC_ID as LEDGER_CONNECTION_PUBLIC_ID, +) +from packages.valory.connections.ledger.tests.conftest import make_ledger_api_connection +from packages.valory.protocols.ledger_api import LedgerApiMessage +from packages.valory.protocols.ledger_api.dialogues import LedgerApiDialogue +from packages.valory.skills.abstract_round_abci.base import AbciAppDB +from packages.valory.skills.abstract_round_abci.behaviours import BaseBehaviour +from packages.valory.skills.abstract_round_abci.models import Requests +from packages.valory.skills.abstract_round_abci.test_tools.integration import ( + IntegrationBaseCase, +) +from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci.behaviours import ( + DummyStartingBehaviour, +) +from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci.rounds import ( + SynchronizedData, +) +from packages.valory.skills.abstract_round_abci.tests.test_tools.base import ( + FSMBehaviourTestToolSetup, +) + + +def simulate_ledger_get_balance_request(test_instance: IntegrationBaseCase) -> None: + """Simulate ledger GET_BALANCE request""" + + ledger_api_dialogues = test_instance.skill.skill_context.ledger_api_dialogues + ledger_api_msg, ledger_api_dialogue = ledger_api_dialogues.create( + counterparty=str(LEDGER_CONNECTION_PUBLIC_ID), + performative=LedgerApiMessage.Performative.GET_BALANCE, + ledger_id="ethereum", + address="0x" + "0" * 40, + ) + ledger_api_dialogue = cast(LedgerApiDialogue, ledger_api_dialogue) + current_behaviour = cast(BaseBehaviour, test_instance.behaviour.current_behaviour) + request_nonce = current_behaviour._get_request_nonce_from_dialogue( # pylint: disable=protected-access + ledger_api_dialogue + ) + cast(Requests, current_behaviour.context.requests).request_id_to_callback[ + request_nonce + ] = current_behaviour.get_callback_request() + current_behaviour.context.outbox.put_message(message=ledger_api_msg) + + +class TestIntegrationBaseCase(FSMBehaviourTestToolSetup): + """TestIntegrationBaseCase""" + + test_cls = IntegrationBaseCase + + def test_instantiation(self) -> None: + """Test instantiation""" + + self.set_path_to_skill() + self.test_cls.make_ledger_api_connection_callable = make_ledger_api_connection + test_instance = cast(IntegrationBaseCase, self.setup_test_cls()) + + assert test_instance + assert test_instance.get_message_from_outbox() is None + assert test_instance.get_message_from_decision_maker_inbox() is None + assert test_instance.process_n_messages(ncycles=0) is tuple() + + expected = "Invalid number of messages in outbox. Expected 1. Found 0." + with pytest.raises(AssertionError, match=expected): + assert test_instance.process_message_cycle() + with pytest.raises(AssertionError, match=expected): + assert test_instance.process_n_messages(ncycles=1) + + def test_process_messages_cycle(self) -> None: + """Test process_message_cycle""" + + self.set_path_to_skill() + self.test_cls.make_ledger_api_connection_callable = make_ledger_api_connection + test_instance = cast(IntegrationBaseCase, self.setup_test_cls()) + + simulate_ledger_get_balance_request(test_instance) + message = test_instance.process_message_cycle( + handler=None, + ) + assert message is None + + simulate_ledger_get_balance_request(test_instance) + # connection error - cannot dynamically mix in an autouse fixture + message = test_instance.process_message_cycle( + handler=test_instance.ledger_handler, + expected_content={"performative": LedgerApiMessage.Performative.ERROR}, + ) + assert message + + def test_process_n_messages(self) -> None: + """Test process_n_messages""" + + self.set_path_to_skill() + self.test_cls.make_ledger_api_connection_callable = make_ledger_api_connection + test_instance = cast(IntegrationBaseCase, self.setup_test_cls()) + + behaviour_id = DummyStartingBehaviour.auto_behaviour_id() + synchronized_data = SynchronizedData( + AbciAppDB(setup_data=dict(participants=[tuple("abcd")])) + ) + + handlers = [test_instance.signing_handler] + expected_content = [ + {"performative": SigningMessage.Performative.SIGNED_MESSAGE} + ] + expected_types = [{"signed_message": SignedMessage}] + + messages = test_instance.process_n_messages( + ncycles=1, + behaviour_id=behaviour_id, + synchronized_data=synchronized_data, + handlers=handlers, # type: ignore + expected_content=expected_content, # type: ignore + expected_types=expected_types, # type: ignore + fail_send_a2a=True, + ) + assert len(messages) == 1 diff --git a/packages/valory/skills/abstract_round_abci/tests/test_tools/test_rounds.py b/packages/valory/skills/abstract_round_abci/tests/test_tools/test_rounds.py new file mode 100644 index 0000000..9fbbde9 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/test_tools/test_rounds.py @@ -0,0 +1,659 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2022-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + + +"""Test the `rounds` test tool module of the skill.""" + +import re +from enum import Enum +from typing import Any, FrozenSet, Generator, List, Optional, Tuple, Type, cast +from unittest.mock import MagicMock + +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st + +from packages.valory.skills.abstract_round_abci.base import ( + AbciAppDB, + BaseSynchronizedData, +) +from packages.valory.skills.abstract_round_abci.test_tools.rounds import ( + BaseCollectDifferentUntilAllRoundTest, + BaseCollectDifferentUntilThresholdRoundTest, + BaseCollectSameUntilAllRoundTest, + BaseCollectSameUntilThresholdRoundTest, + BaseOnlyKeeperSendsRoundTest, + BaseRoundTestClass, + BaseVotingRoundTest, + DummyCollectDifferentUntilAllRound, + DummyCollectDifferentUntilThresholdRound, + DummyCollectSameUntilAllRound, + DummyCollectSameUntilThresholdRound, + DummyEvent, + DummyOnlyKeeperSendsRound, + DummySynchronizedData, + DummyTxPayload, + DummyVotingRound, + MAX_PARTICIPANTS, + get_dummy_tx_payloads, + get_participants, +) +from packages.valory.skills.abstract_round_abci.tests.conftest import profile_name +from packages.valory.skills.abstract_round_abci.tests.test_common import last_iteration + + +settings.load_profile(profile_name) + +# this is how many times we need to iterate before reaching the last iteration for a base test. +BASE_TEST_GEN_ITERATIONS = 4 + + +def test_get_participants() -> None: + """Test `get_participants`.""" + participants = get_participants() + assert isinstance(participants, frozenset) + assert all(isinstance(p, str) for p in participants) + assert len(participants) == MAX_PARTICIPANTS + + +class DummyTxPayloadMatcher: + """A `DummyTxPayload` matcher for assertion comparisons.""" + + expected: DummyTxPayload + + def __init__(self, expected: DummyTxPayload) -> None: + """Initialize the matcher.""" + self.expected = expected + + def __repr__(self) -> str: + """Needs to be implemented for better assertion messages.""" + return ( + "DummyTxPayload(" + f"id={repr(self.expected.id_)}, " + f"round_count={repr(self.expected.round_count)}, " + f"sender={repr(self.expected.sender)}, " + f"value={repr(self.expected.value)}, " + f"vote={repr(self.expected.vote)}" + ")" + ) + + def __eq__(self, other: Any) -> bool: + """The method that will be used for the assertion comparisons.""" + return ( + self.expected.round_count == other.round_count + and self.expected.sender == other.sender + and self.expected.value == other.value + and self.expected.vote == other.vote + ) + + +@given( + st.frozensets(st.text(max_size=200), max_size=100), + st.text(max_size=500), + st.one_of(st.none(), st.booleans()), + st.booleans(), +) +def test_get_dummy_tx_payloads( + participants: FrozenSet[str], + value: str, + vote: Optional[bool], + is_value_none: bool, +) -> None: + """Test `get_dummy_tx_payloads`.""" + expected = [ + DummyTxPayloadMatcher( + DummyTxPayload( + sender=agent, + value=(value or agent) if not is_value_none else value, + vote=vote, + ) + ) + for agent in sorted(participants) + ] + + actual = get_dummy_tx_payloads(participants, value, vote, is_value_none) + + assert len(actual) == len(expected) == len(participants) + assert actual == expected + + +class TestDummyTxPayload: # pylint: disable=too-few-public-methods + """Test class for `DummyTxPayload`""" + + @staticmethod + @given(st.text(max_size=200), st.text(max_size=500), st.booleans()) + def test_properties( + sender: str, + value: str, + vote: bool, + ) -> None: + """Test all the properties.""" + dummy_tx_payload = DummyTxPayload(sender, value, vote) + assert dummy_tx_payload.value == value + assert dummy_tx_payload.vote == vote + assert dummy_tx_payload.data == {"value": value, "vote": vote} + + +class TestDummySynchronizedData: # pylint: disable=too-few-public-methods + """Test class for `DummySynchronizedData`.""" + + @staticmethod + @given(st.lists(st.text(max_size=200), max_size=100)) + def test_most_voted_keeper_address( + most_voted_keeper_address_data: List[str], + ) -> None: + """Test `most_voted_keeper_address`.""" + most_voted_keeper_address_key = "most_voted_keeper_address" + + dummy_synchronized_data = DummySynchronizedData( + db=AbciAppDB( + setup_data={ + most_voted_keeper_address_key: most_voted_keeper_address_data + } + ) + ) + + if len(most_voted_keeper_address_data) == 0: + with pytest.raises( + ValueError, + match=re.escape( + f"'{most_voted_keeper_address_key}' " + "field is not set for this period [0] and no default value was provided.", + ), + ): + _ = dummy_synchronized_data.most_voted_keeper_address + return + + assert ( + dummy_synchronized_data.most_voted_keeper_address + == most_voted_keeper_address_data[-1] + ) + + +class TestBaseRoundTestClass: + """Test `BaseRoundTestClass`.""" + + @staticmethod + def test_test_no_majority_event() -> None: + """Test `_test_no_majority_event`.""" + base_round_test = BaseRoundTestClass() + base_round_test._event_class = DummyEvent # pylint: disable=protected-access + + base_round_test._test_no_majority_event( # pylint: disable=protected-access + MagicMock( + end_block=lambda: ( + MagicMock(), + DummyEvent.NO_MAJORITY, + ) + ) + ) + + @staticmethod + @given(st.integers(min_value=0, max_value=100), st.integers(min_value=1)) + def test_complete_run(iter_count: int, shift: int) -> None: + """Test `_complete_run`.""" + + def dummy_gen() -> Generator[MagicMock, None, None]: + """A dummy generator.""" + return (MagicMock() for _ in range(iter_count)) + + # test with the same number as the generator's contents + gen = dummy_gen() + BaseRoundTestClass._complete_run( # pylint: disable=protected-access + gen, iter_count + ) + + # assert that the generator has been fully consumed + with pytest.raises(StopIteration): + next(gen) + + # test with a larger count than a generator's + with pytest.raises(StopIteration): + BaseRoundTestClass._complete_run( # pylint: disable=protected-access + dummy_gen(), iter_count + shift + ) + + +class BaseTestBase: + """Base class for the Base tests.""" + + gen: Generator + base_round_test: BaseRoundTestClass + base_round_test_cls: Type[BaseRoundTestClass] + test_method_name = "_test_round" + + def setup(self) -> None: + """Setup that is run before each test.""" + self.base_round_test = self.base_round_test_cls() + self.base_round_test._synchronized_data_class = ( # pylint: disable=protected-access + DummySynchronizedData + ) + self.base_round_test.setup() + self.base_round_test._event_class = ( # pylint: disable=protected-access + DummyEvent + ) + + def create_test_gen(self, **kwargs: Any) -> None: + """Create the base test generator.""" + test_method = getattr(self.base_round_test, self.test_method_name) + self.gen = test_method(**kwargs) + + def exhaust_base_test_gen(self) -> None: + """Exhaust the base test generator.""" + for _ in range(BASE_TEST_GEN_ITERATIONS): + next(self.gen) + last_iteration(self.gen) + + def run_test(self, **kwargs: Any) -> None: + """Run a test for a base test.""" + self.create_test_gen(**kwargs) + self.exhaust_base_test_gen() + + +class DummyCollectDifferentUntilAllRoundWithEndBlock( + DummyCollectDifferentUntilAllRound +): + """A `DummyCollectDifferentUntilAllRound` with `end_block` implemented.""" + + def __init__(self, dummy_exit_event: DummyEvent, *args: Any, **kwargs: Any): + """Initialize the dummy class.""" + super().__init__(*args, **kwargs) + self.dummy_exit_event = dummy_exit_event + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: + """A dummy `end_block` implementation.""" + if self.collection_threshold_reached and self.dummy_exit_event is not None: + return ( + cast( + DummySynchronizedData, + self.synchronized_data.update( + most_voted_keeper_address=list(self.collection.keys()) + ), + ), + self.dummy_exit_event, + ) + return None + + +class TestBaseCollectDifferentUntilAllRoundTest(BaseTestBase): + """Test `BaseCollectDifferentUntilAllRoundTest`.""" + + base_round_test: BaseCollectDifferentUntilAllRoundTest + base_round_test_cls = BaseCollectDifferentUntilAllRoundTest + + @given( + st.one_of(st.none(), st.sampled_from(DummyEvent)), + ) + def test_test_round(self, exit_event: DummyEvent) -> None: + """Test `_test_round`.""" + test_round = DummyCollectDifferentUntilAllRoundWithEndBlock( + exit_event, + self.base_round_test.synchronized_data, + context=MagicMock(), + ) + round_payloads = [ + DummyTxPayload(f"agent_{i}", str(i)) for i in range(MAX_PARTICIPANTS) + ] + synchronized_data_attr_checks = [ + lambda _synchronized_data: _synchronized_data.most_voted_keeper_address + ] + + self.run_test( + test_round=test_round, + round_payloads=round_payloads, + synchronized_data_update_fn=lambda synchronized_data, _: synchronized_data.update( + most_voted_keeper_address=[ + f"agent_{i}" for i in range(MAX_PARTICIPANTS) + ] + ), + synchronized_data_attr_checks=synchronized_data_attr_checks, + exit_event=exit_event, + ) + + +class DummyCollectSameUntilAllRoundWithEndBlock(DummyCollectSameUntilAllRound): + """A `DummyCollectSameUntilAllRound` with `end_block` implemented.""" + + def __init__(self, dummy_exit_event: DummyEvent, *args: Any, **kwargs: Any): + """Initialize the dummy class.""" + super().__init__(*args, **kwargs) + self.dummy_exit_event = dummy_exit_event + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: + """A dummy `end_block` implementation.""" + if self.collection_threshold_reached: + return ( + cast( + DummySynchronizedData, + self.synchronized_data.update( + most_voted_keeper_address=self.common_payload + ), + ), + self.dummy_exit_event, + ) + return None + + +class TestBaseCollectSameUntilAllRoundTest(BaseTestBase): + """Test `BaseCollectSameUntilAllRoundTest`.""" + + base_round_test: BaseCollectSameUntilAllRoundTest + base_round_test_cls: Type[ + BaseCollectSameUntilAllRoundTest + ] = BaseCollectSameUntilAllRoundTest + + @given( + st.sampled_from(DummyEvent), + st.text(max_size=500), + st.booleans(), + ) + def test_test_round( + self, exit_event: DummyEvent, common_value: str, finished: bool + ) -> None: + """Test `_test_round`.""" + test_round = DummyCollectSameUntilAllRoundWithEndBlock( + exit_event, + self.base_round_test.synchronized_data, + context=MagicMock(), + ) + round_payloads = { + f"test{i}": DummyTxPayload(f"agent_{i}", common_value) + for i in range(MAX_PARTICIPANTS) + } + synchronized_data_attr_checks = [ + lambda _synchronized_data: _synchronized_data.most_voted_keeper_address + ] + + self.run_test( + test_round=test_round, + round_payloads=round_payloads, + synchronized_data_update_fn=lambda synchronized_data, _: synchronized_data.update( + most_voted_keeper_address=common_value + ), + synchronized_data_attr_checks=synchronized_data_attr_checks, + most_voted_payload=common_value, + exit_event=exit_event, + finished=finished, + ) + + +class DummyCollectSameUntilThresholdRoundWithEndBlock( + DummyCollectSameUntilThresholdRound +): + """A `DummyCollectSameUntilThresholdRound` with `end_block` overriden.""" + + def __init__(self, dummy_exit_event: DummyEvent, *args: Any, **kwargs: Any): + """Initialize the dummy class.""" + super().__init__(*args, **kwargs) + self.dummy_exit_event = dummy_exit_event + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: + """A dummy `end_block` override.""" + if self.threshold_reached: + return ( + cast( + DummySynchronizedData, + self.synchronized_data.update( + most_voted_keeper_address=self.most_voted_payload + ), + ), + self.dummy_exit_event, + ) + if not self.is_majority_possible( + self.collection, self.synchronized_data.nb_participants + ): + return self.synchronized_data, DummyEvent.NO_MAJORITY + return None + + +class TestBaseCollectSameUntilThresholdRoundTest(BaseTestBase): + """Test `BaseCollectSameUntilThresholdRoundTest`.""" + + base_round_test: BaseCollectSameUntilThresholdRoundTest + base_round_test_cls: Type[ + BaseCollectSameUntilThresholdRoundTest + ] = BaseCollectSameUntilThresholdRoundTest + + @given( + st.sampled_from(DummyEvent), + st.text(max_size=500), + ) + def test_test_round(self, exit_event: DummyEvent, most_voted_payload: str) -> None: + """Test `_test_round`.""" + test_round = DummyCollectSameUntilThresholdRoundWithEndBlock( + exit_event, + self.base_round_test.synchronized_data, + context=MagicMock(), + ) + round_payloads = { + f"test{i}": DummyTxPayload(f"agent_{i}", most_voted_payload) + for i in range(MAX_PARTICIPANTS) + } + synchronized_data_attr_checks = [ + lambda _synchronized_data: _synchronized_data.most_voted_keeper_address + ] + + self.run_test( + test_round=test_round, + round_payloads=round_payloads, + synchronized_data_update_fn=lambda synchronized_data, _: synchronized_data.update( + most_voted_keeper_address=most_voted_payload + ), + synchronized_data_attr_checks=synchronized_data_attr_checks, + most_voted_payload=most_voted_payload, + exit_event=exit_event, + ) + + +class DummyOnlyKeeperSendsRoundTest(DummyOnlyKeeperSendsRound): + """A `DummyOnlyKeeperSendsRound` with `end_block` implemented.""" + + def __init__(self, dummy_exit_event: DummyEvent, *args: Any, **kwargs: Any): + """Initialize the dummy class.""" + super().__init__(*args, **kwargs) + self.dummy_exit_event = dummy_exit_event + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: + """A dummy `end_block` implementation.""" + if self.keeper_payload is not None and any( + [val is not None for val in self.keeper_payload.values] + ): + return ( + cast( + DummySynchronizedData, + self.synchronized_data.update( + blacklisted_keepers=self.keeper_payload.values[0] + ), + ), + self.dummy_exit_event, + ) + return None + + +class TestBaseOnlyKeeperSendsRoundTest(BaseTestBase): + """Test `BaseOnlyKeeperSendsRoundTest`.""" + + base_round_test: BaseOnlyKeeperSendsRoundTest + base_round_test_cls: Type[ + BaseOnlyKeeperSendsRoundTest + ] = BaseOnlyKeeperSendsRoundTest + most_voted_keeper_address: str = "agent_0" + + def setup(self) -> None: + """Setup that is run before each test.""" + super().setup() + self.base_round_test.synchronized_data.update( + most_voted_keeper_address=self.most_voted_keeper_address + ) + + @given( + st.sampled_from(DummyEvent), + st.text(), + ) + def test_test_round(self, exit_event: DummyEvent, keeper_value: str) -> None: + """Test `_test_round`.""" + test_round = DummyOnlyKeeperSendsRoundTest( + exit_event, + self.base_round_test.synchronized_data, + context=MagicMock(), + ) + keeper_payload = DummyTxPayload(self.most_voted_keeper_address, keeper_value) + synchronized_data_attr_checks = [ + lambda _synchronized_data: _synchronized_data.blacklisted_keepers + ] + + self.run_test( + test_round=test_round, + keeper_payloads=keeper_payload, + synchronized_data_update_fn=lambda synchronized_data, _: synchronized_data.update( + blacklisted_keepers=keeper_value + ), + synchronized_data_attr_checks=synchronized_data_attr_checks, + exit_event=exit_event, + ) + + +class DummyBaseVotingRoundTestWithEndBlock(DummyVotingRound): + """A `DummyVotingRound` with `end_block` overriden.""" + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: + """A dummy `end_block` override.""" + if self.positive_vote_threshold_reached: + synchronized_data = cast( + DummySynchronizedData, + self.synchronized_data.update( + is_keeper_set=bool(self.collection), + ), + ) + return synchronized_data, DummyEvent.DONE + if self.negative_vote_threshold_reached: + return self.synchronized_data, DummyEvent.NEGATIVE + if self.none_vote_threshold_reached: + return self.synchronized_data, DummyEvent.NONE + if not self.is_majority_possible( + self.collection, self.synchronized_data.nb_participants + ): + return self.synchronized_data, DummyEvent.NO_MAJORITY + return None + + +class TestBaseVotingRoundTest(BaseTestBase): + """Test `BaseVotingRoundTest`.""" + + base_round_test: BaseVotingRoundTest + base_round_test_cls: Type[BaseVotingRoundTest] = BaseVotingRoundTest + + @given( + st.one_of(st.none(), st.booleans()), + ) + def test_test_round(self, is_keeper_set: Optional[bool]) -> None: + """Test `_test_round`.""" + if is_keeper_set is None: + exit_event = DummyEvent.NONE + self.test_method_name = "_test_voting_round_none" + elif is_keeper_set: + exit_event = DummyEvent.DONE + self.test_method_name = "_test_voting_round_positive" + else: + exit_event = DummyEvent.NEGATIVE + self.test_method_name = "_test_voting_round_negative" + + test_round = DummyBaseVotingRoundTestWithEndBlock( + self.base_round_test.synchronized_data, + context=MagicMock(), + ) + round_payloads = { + f"test{i}": DummyTxPayload(f"agent_{i}", value="", vote=is_keeper_set) + for i in range(MAX_PARTICIPANTS) + } + synchronized_data_attr_checks = [ + lambda _synchronized_data: _synchronized_data.is_keeper_set + ] + + self.run_test( + test_round=test_round, + round_payloads=round_payloads, + synchronized_data_update_fn=lambda synchronized_data, _: synchronized_data.update( + is_keeper_set=is_keeper_set + ), + synchronized_data_attr_checks=synchronized_data_attr_checks, + exit_event=exit_event, + ) + + +class DummyCollectDifferentUntilThresholdRoundWithEndBlock( + DummyCollectDifferentUntilThresholdRound +): + """A `DummyCollectDifferentUntilThresholdRound` with `end_block` implemented.""" + + def __init__(self, dummy_exit_event: DummyEvent, *args: Any, **kwargs: Any): + """Initialize the dummy class.""" + super().__init__(*args, **kwargs) + self.dummy_exit_event = dummy_exit_event + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: + """A dummy `end_block` implementation.""" + if self.collection_threshold_reached and self.dummy_exit_event is not None: + return ( + cast( + DummySynchronizedData, + self.synchronized_data.update( + most_voted_keeper_address=list(self.collection.keys()) + ), + ), + self.dummy_exit_event, + ) + return None + + +class TestBaseCollectDifferentUntilThresholdRoundTest(BaseTestBase): + """Test `BaseCollectDifferentUntilThresholdRoundTest`.""" + + base_round_test: BaseCollectDifferentUntilThresholdRoundTest + base_round_test_cls: Type[ + BaseCollectDifferentUntilThresholdRoundTest + ] = BaseCollectDifferentUntilThresholdRoundTest + + @given(st.sampled_from(DummyEvent)) + def test_test_round(self, exit_event: DummyEvent) -> None: + """Test `_test_round`.""" + test_round = DummyCollectDifferentUntilThresholdRoundWithEndBlock( + exit_event, + self.base_round_test.synchronized_data, + context=MagicMock(), + ) + round_payloads = { + f"test{i}": DummyTxPayload(f"agent_{i}", str(i)) + for i in range(MAX_PARTICIPANTS) + } + synchronized_data_attr_checks = [ + lambda _synchronized_data: _synchronized_data.most_voted_keeper_address + ] + + self.run_test( + test_round=test_round, + round_payloads=round_payloads, + synchronized_data_update_fn=lambda synchronized_data, _: synchronized_data.update( + most_voted_keeper_address=[ + f"agent_{i}" for i in range(MAX_PARTICIPANTS) + ] + ), + synchronized_data_attr_checks=synchronized_data_attr_checks, + exit_event=exit_event, + ) diff --git a/packages/valory/skills/abstract_round_abci/tests/test_utils.py b/packages/valory/skills/abstract_round_abci/tests/test_utils.py new file mode 100644 index 0000000..3f41273 --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/tests/test_utils.py @@ -0,0 +1,307 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test the utils.py module of the skill.""" + +from collections import defaultdict +from string import printable +from typing import Any, Dict, List, Tuple, Type +from unittest import mock + +import pytest +from hypothesis import assume, given, settings +from hypothesis import strategies as st + +from packages.valory.skills.abstract_round_abci.tests.conftest import profile_name +from packages.valory.skills.abstract_round_abci.utils import ( + DEFAULT_TENDERMINT_P2P_PORT, + KeyType, + MAX_UINT64, + ValueType, + VerifyDrand, + consensus_threshold, + filter_negative, + get_data_from_nested_dict, + get_value_with_type, + inverse, + is_json_serializable, + is_primitive_or_none, + parse_tendermint_p2p_url, +) + + +settings.load_profile(profile_name) + + +# pylint: skip-file + + +DRAND_PUBLIC_KEY: str = "868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31" + +DRAND_VALUE = { + "round": 1416669, + "randomness": "f6be4bf1fa229f22340c1a5b258f809ac4af558200775a67dacb05f0cb258a11", + "signature": ( + "b44d00516f46da3a503f9559a634869b6dc2e5d839e46ec61a090e3032172954929a5" + "d9bd7197d7739fe55db770543c71182562bd0ad20922eb4fe6b8a1062ed21df3b68de" + "44694eb4f20b35262fa9d63aa80ad3f6172dd4d33a663f21179604" + ), + "previous_signature": ( + "903c60a4b937a804001032499a855025573040cb86017c38e2b1c3725286756ce8f33" + "61188789c17336beaf3f9dbf84b0ad3c86add187987a9a0685bc5a303e37b008fba8c" + "44f02a416480dd117a3ff8b8075b1b7362c58af195573623187463" + ), +} + + +class TestVerifyDrand: + """Test DrandVerify.""" + + drand_check: VerifyDrand + + def setup( + self, + ) -> None: + """Setup test.""" + self.drand_check = VerifyDrand() + + def test_verify( + self, + ) -> None: + """Test verify method.""" + + result, error = self.drand_check.verify(DRAND_VALUE, DRAND_PUBLIC_KEY) + assert result + assert error is None + + def test_verify_fails( + self, + ) -> None: + """Test verify method.""" + + drand_value = DRAND_VALUE.copy() + del drand_value["randomness"] + result, error = self.drand_check.verify(drand_value, DRAND_PUBLIC_KEY) + assert not result + assert error == "DRAND dict is missing value for 'randomness'" + + drand_value = DRAND_VALUE.copy() + drand_value["randomness"] = "".join( + list(drand_value["randomness"])[:-1] + ["0"] # type: ignore + ) + result, error = self.drand_check.verify(drand_value, DRAND_PUBLIC_KEY) + assert not result + assert error == "Failed randomness hash check." + + drand_value = DRAND_VALUE.copy() + with mock.patch.object( + self.drand_check, "_verify_signature", return_value=False + ): + result, error = self.drand_check.verify(drand_value, DRAND_PUBLIC_KEY) + + assert not result + assert error == "Failed bls.Verify check." + + @pytest.mark.parametrize("value", (-1, MAX_UINT64 + 1)) + def test_negative_and_overflow(self, value: int) -> None: + """Test verify method.""" + with pytest.raises(ValueError): + self.drand_check._int_to_bytes_big(value) + + +@given(st.integers(min_value=0, max_value=MAX_UINT64)) +def test_verify_int_to_bytes_big_fuzz(integer: int) -> None: + """Test VerifyDrand.""" + + VerifyDrand._int_to_bytes_big(integer) + + +@pytest.mark.parametrize("integer", [-1, MAX_UINT64 + 1]) +def test_verify_int_to_bytes_big_raises(integer: int) -> None: + """Test VerifyDrand._int_to_bytes_big""" + + expected = "VerifyDrand can only handle positive numbers representable with 8 bytes" + with pytest.raises(ValueError, match=expected): + VerifyDrand._int_to_bytes_big(integer) + + +@given(st.binary()) +def test_verify_randomness_hash_fuzz(input_bytes: bytes) -> None: + """Test VerifyDrand._verify_randomness_hash""" + + VerifyDrand._verify_randomness_hash(input_bytes, input_bytes) + + +@given( + st.lists(st.text(), min_size=1, max_size=50), + st.binary(), + st.characters(), +) +def test_get_data_from_nested_dict( + nested_keys: List[str], final_value: bytes, separator: str +) -> None: + """Test `get_data_from_nested_dict`""" + assume(not any(separator in key for key in nested_keys)) + + def create_nested_dict() -> defaultdict: + """Recursively create a nested dict of arbitrary size.""" + return defaultdict(create_nested_dict) + + nested_dict = create_nested_dict() + key_access = (f"[nested_keys[{i}]]" for i in range(len(nested_keys))) + expression = "nested_dict" + "".join(key_access) + expression += " = final_value" + exec(expression) # nosec + + serialized_keys = separator.join(nested_keys) + actual = get_data_from_nested_dict(nested_dict, serialized_keys, separator) + assert actual == final_value + + +@pytest.mark.parametrize( + "type_name, type_, value", + ( + ("str", str, "1"), + ("int", int, 1), + ("float", float, 1.1), + ("dict", dict, {1: 1}), + ("list", list, [1]), + ("non_existent", None, 1), + ), +) +def test_get_value_with_type(type_name: str, type_: Type, value: Any) -> None: + """Test `get_value_with_type`""" + if type_ is None: + with pytest.raises( + AttributeError, match=f"module 'builtins' has no attribute '{type_name}'" + ): + get_value_with_type(value, type_name) + return + + actual = get_value_with_type(value, type_name) + assert type(actual) == type_ + assert actual == value + + +@pytest.mark.parametrize( + ("url", "expected_output"), + ( + ("localhost", ("localhost", DEFAULT_TENDERMINT_P2P_PORT)), + ("localhost:80", ("localhost", 80)), + ("some.random.host:80", ("some.random.host", 80)), + ("1.1.1.1", ("1.1.1.1", DEFAULT_TENDERMINT_P2P_PORT)), + ("1.1.1.1:80", ("1.1.1.1", 80)), + ), +) +def test_parse_tendermint_p2p_url(url: str, expected_output: Tuple[str, int]) -> None: + """Test `parse_tendermint_p2p_url` method.""" + + assert parse_tendermint_p2p_url(url=url) == expected_output + + +@given( + st.one_of(st.none(), st.integers(), st.floats(), st.text(), st.booleans()), + st.one_of( + st.nothing(), + st.frozensets(st.integers()), + st.sets(st.integers()), + st.lists(st.integers()), + st.dictionaries(st.integers(), st.integers()), + st.dates(), + st.complex_numbers(), + st.just(object()), + ), +) +def test_is_primitive_or_none(valid_obj: Any, invalid_obj: Any) -> None: + """Test `is_primitive_or_none`.""" + assert is_primitive_or_none(valid_obj) + assert not is_primitive_or_none(invalid_obj) + + +@given( + st.recursive( + st.none() | st.booleans() | st.floats() | st.text(printable), + lambda children: st.lists(children) + | st.dictionaries(st.text(printable), children), + ), + st.one_of( + st.nothing(), + st.frozensets(st.integers()), + st.sets(st.integers()), + st.dates(), + st.complex_numbers(), + st.just(object()), + ), +) +def test_is_json_serializable(valid_obj: Any, invalid_obj: Any) -> None: + """Test `is_json_serializable`.""" + assert is_json_serializable(valid_obj) + assert not is_json_serializable(invalid_obj) + + +@given( + positive=st.dictionaries(st.text(), st.integers(min_value=0)), + negative=st.dictionaries(st.text(), st.integers(max_value=-1)), +) +def test_filter_negative(positive: Dict[str, int], negative: Dict[str, int]) -> None: + """Test `filter_negative`.""" + assert len(tuple(filter_negative(positive))) == 0 + assert set(filter_negative(negative)) == set(negative.keys()) + + +@pytest.mark.parametrize( + "nb, threshold", + ((1, 1), (2, 2), (3, 3), (4, 3), (5, 4), (6, 5), (100, 67), (300, 201)), +) +def test_consensus_threshold(nb: int, threshold: int) -> None: + """Test `consensus_threshold`.""" + assert consensus_threshold(nb) == threshold + + +@pytest.mark.parametrize( + "dict_, expected", + ( + ({}, {}), + ( + {"test": "this", "which?": "this"}, + {"this": ["test", "which?"]}, + ), + ( + {"test": "this", "which?": "this", "hm": "ok"}, + {"this": ["test", "which?"], "ok": ["hm"]}, + ), + ( + {"test": "this", "hm": "ok"}, + {"this": ["test"], "ok": ["hm"]}, + ), + ( + {"test": "this", "hm": "ok", "ok": "ok"}, + {"this": ["test"], "ok": ["hm", "ok"]}, + ), + ( + {"test": "this", "which?": "this", "hm": "ok", "ok": "ok"}, + {"this": ["test", "which?"], "ok": ["hm", "ok"]}, + ), + ), +) +def test_inverse( + dict_: Dict[KeyType, ValueType], expected: Dict[ValueType, List[KeyType]] +) -> None: + """Test `inverse`.""" + assert inverse(dict_) == expected diff --git a/packages/valory/skills/abstract_round_abci/utils.py b/packages/valory/skills/abstract_round_abci/utils.py new file mode 100644 index 0000000..864658c --- /dev/null +++ b/packages/valory/skills/abstract_round_abci/utils.py @@ -0,0 +1,504 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains utility functions for the 'abstract_round_abci' skill.""" + +import builtins +import collections +import dataclasses +import sys +import types +import typing +from hashlib import sha256 +from math import ceil +from typing import ( + Any, + Dict, + FrozenSet, + Iterator, + List, + Optional, + Set, + Tuple, + Type, + TypeVar, + Union, + cast, +) +from unittest.mock import MagicMock + +import typing_extensions +from eth_typing.bls import BLSPubkey, BLSSignature +from py_ecc.bls import G2Basic as bls +from typing_extensions import Literal, TypeGuard, TypedDict + + +MAX_UINT64 = 2**64 - 1 +DEFAULT_TENDERMINT_P2P_PORT = 26656 + + +class VerifyDrand: # pylint: disable=too-few-public-methods + """ + Tool to verify Randomness retrieved from various external APIs. + + The ciphersuite used is BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_ + + cryptographic-specification section in https://drand.love/docs/specification/ + https://github.com/ethereum/py_ecc + """ + + @classmethod + def _int_to_bytes_big(cls, value: int) -> bytes: + """Convert int to bytes.""" + if value < 0 or value > MAX_UINT64: + raise ValueError( + "VerifyDrand can only handle positive numbers representable with 8 bytes" + ) + return int.to_bytes(value, 8, byteorder="big", signed=False) + + @classmethod + def _verify_randomness_hash(cls, randomness: bytes, signature: bytes) -> bool: + """Verify randomness hash.""" + return sha256(signature).digest() == randomness + + @classmethod + def _verify_signature( + cls, + pubkey: Union[BLSPubkey, bytes], + message: bytes, + signature: Union[BLSSignature, bytes], + ) -> bool: + """Verify randomness signature.""" + return bls.Verify( + cast(BLSPubkey, pubkey), message, cast(BLSSignature, signature) + ) + + def verify(self, data: Dict, pubkey: str) -> Tuple[bool, Optional[str]]: + """ + Verify drand value retried from external APIs. + + :param data: dictionary containing drand parameters. + :param pubkey: league of entropy public key + public-endpoints section in https://drand.love/developer/http-api/ + :returns: bool, error message + """ + + encoded_pubkey = bytes.fromhex(pubkey) + try: + randomness = data["randomness"] + signature = data["signature"] + round_value = int(data["round"]) + except KeyError as e: + return False, f"DRAND dict is missing value for {e}" + + previous_signature = data.pop("previous_signature", "") + encoded_randomness = bytes.fromhex(randomness) + encoded_signature = bytes.fromhex(signature) + int_encoded_round = self._int_to_bytes_big(round_value) + encoded_previous_signature = bytes.fromhex(previous_signature) + + if not self._verify_randomness_hash(encoded_randomness, encoded_signature): + return False, "Failed randomness hash check." + + msg_b = encoded_previous_signature + int_encoded_round + msg_hash_b = sha256(msg_b).digest() + + if not self._verify_signature(encoded_pubkey, msg_hash_b, encoded_signature): + return False, "Failed bls.Verify check." + + return True, None + + +def get_data_from_nested_dict( + nested_dict: Dict, keys: str, separator: str = ":" +) -> Any: + """Gets content from a nested dictionary, using serialized response keys which are split by a given separator. + + :param nested_dict: the nested dictionary to get the content from + :param keys: the keys to use on the nested dictionary in order to get the content + :param separator: the separator to use in order to get the keys list. + Choose the separator carefully, so that it does not conflict with any character of the keys. + + :returns: the content result + """ + parsed_keys = keys.split(separator) + for key in parsed_keys: + nested_dict = nested_dict[key] + return nested_dict + + +def get_value_with_type(value: Any, type_name: str) -> Any: + """Get the given value as the specified type.""" + return getattr(builtins, type_name)(value) + + +def parse_tendermint_p2p_url(url: str) -> Tuple[str, int]: + """Parse tendermint P2P url.""" + hostname, *_port = url.split(":") + if len(_port) > 0: + port_str, *_ = _port + port = int(port_str) + else: + port = DEFAULT_TENDERMINT_P2P_PORT + + return hostname, port + + +## +# Typing utils - to be extracted to open-aea +## + + +try: + # Python >=3.8 should have these functions already + from typing import get_args as _get_args # pylint: disable=ungrouped-imports + from typing import get_origin as _get_origin # pylint: disable=ungrouped-imports +except ImportError: # pragma: nocover + # Python 3.7 + def _get_origin(tp): # type: ignore + """Copied from the Python 3.8 typing module""" + if isinstance(tp, typing._GenericAlias): # pylint: disable=protected-access + return tp.__origin__ + if tp is typing.Generic: + return typing.Generic + return None + + def _get_args(tp): # type: ignore + """Copied from the Python 3.8 typing module""" + if isinstance(tp, typing._GenericAlias): # pylint: disable=protected-access + res = tp.__args__ + if get_origin(tp) is collections.abc.Callable and res[0] is not Ellipsis: + res = (list(res[:-1]), res[-1]) + return res + return () + + +def get_origin(tp): # type: ignore + """ + Get the unsubscripted version of a type. + + This supports generic types, Callable, Tuple, Union, Literal, Final and + ClassVar. Returns None for unsupported types. + Examples: + get_origin(Literal[42]) is Literal + get_origin(int) is None + get_origin(ClassVar[int]) is ClassVar + get_origin(Generic) is Generic + get_origin(Generic[T]) is Generic + get_origin(Union[T, int]) is Union + get_origin(List[Tuple[T, T]][int]) == list + """ + return _get_origin(tp) + + +def get_args(tp): # type: ignore + """ + Get type arguments with all substitutions performed. + + For unions, basic simplifications used by Union constructor are performed. + Examples: + get_args(Dict[str, int]) == (str, int) + get_args(int) == () + get_args(Union[int, Union[T, int], str][int]) == (int, str) + get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int]) + get_args(Callable[[], T][int]) == ([], int) + """ + return _get_args(tp) + + +## +# The following is borrowed from https://github.com/tamuhey/dataclass_utils/blob/81580d2c0c285081db06be02b4ecdd125532bef5/dataclass_utils/type_checker.py#L152 +## + + +def is_pep604_union(ty: Type[Any]) -> bool: + """Check if a type is a PEP 604 union.""" + return sys.version_info >= (3, 10) and ty is types.UnionType # type: ignore # noqa: E721 # pylint: disable=no-member + + +def _path_to_str(path: List[str]) -> str: + """Convert a path to a string.""" + return " -> ".join(reversed(path)) + + +class AutonomyTypeError(TypeError): + """Type Error for the Autonomy type check system.""" + + def __init__( + self, + ty: Type[Any], + value: Any, + path: Optional[List[str]] = None, + ): + """Initialize AutonomyTypeError.""" + self.ty = ty + self.value = value + self.path = path or [] + super().__init__() + + def __str__(self) -> str: + """Get string representation of AutonomyTypeError.""" + path = _path_to_str(self.path) + msg = f"Error in field '{path}'. Expected type {self.ty}, got {type(self.value)} (value: {self.value})" + return msg + + +Result = Optional[AutonomyTypeError] # returns error context + + +def check( # pylint: disable=too-many-return-statements + value: Any, ty: Type[Any] +) -> Result: + """ + Check a value against a type. + + # Examples + >>> assert is_error(check(1, str)) + >>> assert not is_error(check(1, int)) + >>> assert is_error(check(1, list)) + >>> assert is_error(check(1.3, int)) + >>> assert is_error(check(1.3, Union[str, int])) + """ + if isinstance(value, MagicMock): + # testing - any magic value is ignored + return None + if not isinstance(value, type) and dataclasses.is_dataclass(ty): + # dataclass + return check_dataclass(value, ty) + if is_typeddict(ty): + # should use `typing.is_typeddict` in future + return check_typeddict(value, ty) + to = get_origin(ty) + if to is not None: + # generics + err = check(value, to) + if is_error(err): + return err + + if to is list or to is set or to is frozenset: + err = check_mono_container(value, ty) + elif to is dict: + err = check_dict(value, ty) # type: ignore + elif to is tuple: + err = check_tuple(value, ty) + elif to is Literal: + err = check_literal(value, ty) + elif to is Union or is_pep604_union(to): + err = check_union(value, ty) + elif to is type: + err = check_class(value, ty) + return err + if isinstance(ty, type): + # concrete type + if is_pep604_union(ty): + pass # pragma: no cover + elif issubclass(ty, bool): + if not isinstance(value, ty): + return AutonomyTypeError(ty=ty, value=value) + elif issubclass(ty, int): # For boolean + return check_int(value, ty) + elif ty is typing.Any: + # `isinstance(value, typing.Any) fails on python 3.11` + # https://stackoverflow.com/questions/68031358/typeerror-typing-any-cannot-be-used-with-isinstance + pass + elif not isinstance(value, ty): + return AutonomyTypeError(ty=ty, value=value) + return None + + +def check_class(value: Any, ty: Type[Any]) -> Result: + """Check class type.""" + if not issubclass(value, get_args(ty)): + return AutonomyTypeError(ty=ty, value=value) + return None + + +def check_int(value: Any, ty: Type[Any]) -> Result: + """Check int type.""" + if isinstance(value, bool) or not isinstance(value, ty): + return AutonomyTypeError(ty=ty, value=value) + return None + + +def check_literal(value: Any, ty: Type[Any]) -> Result: + """Check literal type.""" + if all(value != t for t in get_args(ty)): + return AutonomyTypeError(ty=ty, value=value) + return None + + +def check_tuple(value: Any, ty: Type[Tuple[Any, ...]]) -> Result: + """Check tuple type.""" + types_ = get_args(ty) + if len(types_) == 2 and types_[1] == ...: + # arbitrary length tuple (e.g. Tuple[int, ...]) + for v in value: + err = check(v, types_[0]) + if is_error(err): + return err + return None + + if len(value) != len(types_): + return AutonomyTypeError(ty=ty, value=value) + for v, t in zip(value, types_): + err = check(v, t) + if is_error(err): + return err + return None + + +def check_union(value: Any, ty: Type[Any]) -> Result: + """Check union type.""" + if any(not is_error(check(value, t)) for t in get_args(ty)): + return None + return AutonomyTypeError(ty=ty, value=value) + + +def check_mono_container( + value: Any, ty: Union[Type[List[Any]], Type[Set[Any]], Type[FrozenSet[Any]]] +) -> Result: + """Check mono container type.""" + ty_item = get_args(ty)[0] + for v in value: + err = check(v, ty_item) + if is_error(err): + return err + return None + + +def check_dict(value: Dict[Any, Any], ty: Type[Dict[Any, Any]]) -> Result: + """Check dict type.""" + args = get_args(ty) + ty_key = args[0] + ty_item = args[1] + for k, v in value.items(): + err = check(k, ty_key) + if is_error(err): + return err + err = check(v, ty_item) + if err is not None: + err.path.append(k) + return err + return None + + +def check_dataclass(value: Any, ty: Type[Any]) -> Result: + """Check dataclass type.""" + if not dataclasses.is_dataclass(value): + return AutonomyTypeError(ty, value) + for k, ty_ in typing.get_type_hints(ty).items(): + v = getattr(value, k) + err = check(v, ty_) + if err is not None: + err.path.append(k) + return err + return None + + +def check_typeddict(value: Any, ty: Type[Any]) -> Result: + """Check typeddict type.""" + if not isinstance(value, dict): + return AutonomyTypeError(ty, value) # pragma: no cover + is_total: bool = ty.__total__ # type: ignore + for k, ty_ in typing.get_type_hints(ty).items(): + if k not in value: + if is_total: + return AutonomyTypeError(ty_, value, [k]) + continue + v = value[k] + err = check(v, ty_) + if err is not None: + err.path.append(k) + return err + return None + + +# TODO: incorporate +def is_typevar(ty: Type[Any]) -> TypeGuard[TypeVar]: + """Check typevar.""" + return isinstance(ty, TypeVar) # pragma: no cover + + +def is_error(ret: Result) -> TypeGuard[AutonomyTypeError]: + """Check error.""" + return ret is not None + + +def is_typeddict(ty: Type[Any]) -> TypeGuard[Type[TypedDict]]: # type: ignore + """Check typeddict.""" + # TODO: Should use `typing.is_typeddict` in future + # or, use publich API + T = "_TypedDictMeta" + for mod in [typing, typing_extensions]: + if hasattr(mod, T) and isinstance(ty, getattr(mod, T)): + return True + return False + + +def check_type(name: str, value: Any, type_hint: Any) -> None: + """Check value against type hint recursively""" + err = check(value, type_hint) + if err is not None: + err.path.append(name) + raise err + + +def is_primitive_or_none(obj: Any) -> bool: + """Checks if the given object is a primitive type or `None`.""" + primitives = (bool, int, float, str) + return isinstance(obj, primitives) or obj is None + + +def is_json_serializable(obj: Any) -> bool: + """Checks if the given object is json serializable.""" + if isinstance(obj, (tuple, list)): + return all(is_json_serializable(x) for x in obj) + if isinstance(obj, dict): + return all( + is_primitive_or_none(k) and is_json_serializable(v) for k, v in obj.items() + ) + + return is_primitive_or_none(obj) + + +def filter_negative(mapping: Dict[str, int]) -> Iterator[str]: + """Return the keys of a dictionary for which the values are negative integers.""" + return (key for key, number in mapping.items() if number < 0) + + +def consensus_threshold(nb: int) -> int: + """ + Get consensus threshold. + + :param nb: the number of participants + :return: the consensus threshold + """ + return ceil((2 * nb + 1) / 3) + + +KeyType = TypeVar("KeyType") +ValueType = TypeVar("ValueType") + + +def inverse(dict_: Dict[KeyType, ValueType]) -> Dict[ValueType, List[KeyType]]: + """Get the inverse of a dictionary.""" + inverse_: Dict[ValueType, List[KeyType]] = {val: [] for val in dict_.values()} + for key, value in dict_.items(): + inverse_[value].append(key) + return inverse_ diff --git a/packages/valory/skills/ipfs_package_downloader/__init__.py b/packages/valory/skills/ipfs_package_downloader/__init__.py new file mode 100644 index 0000000..bb1ff74 --- /dev/null +++ b/packages/valory/skills/ipfs_package_downloader/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the implementation of the task execution skill.""" + +from aea.configurations.base import PublicId + + +PUBLIC_ID = PublicId.from_str("valory/ipfs_package_downloader:0.1.0") diff --git a/packages/valory/skills/ipfs_package_downloader/behaviours.py b/packages/valory/skills/ipfs_package_downloader/behaviours.py new file mode 100644 index 0000000..67cf594 --- /dev/null +++ b/packages/valory/skills/ipfs_package_downloader/behaviours.py @@ -0,0 +1,215 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2023-2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains the implementation of a custom component's management.""" + +import time +from asyncio import Future +from typing import Any, Callable, Dict, Optional, Tuple, cast + +import yaml +from aea.protocols.base import Message +from aea.protocols.dialogue.base import Dialogue +from aea.skills.behaviours import SimpleBehaviour + +from packages.valory.connections.ipfs.connection import IpfsDialogues +from packages.valory.connections.ipfs.connection import PUBLIC_ID as IPFS_CONNECTION_ID +from packages.valory.protocols.ipfs import IpfsMessage +from packages.valory.protocols.ipfs.dialogues import IpfsDialogue +from packages.valory.skills.ipfs_package_downloader.models import Params + + +COMPONENT_YAML_STORE_KEY = "component_yaml" +ENTRY_POINT_STORE_KEY = "entry_point" +CALLABLES_STORE_KEY = "callables" + + +class IpfsPackageDownloader(SimpleBehaviour): + """A class to download packages from IPFS.""" + + def __init__(self, **kwargs: Any): + """Initialise the agent.""" + super().__init__(**kwargs) + self._executing_task: Optional[Dict[str, Optional[float]]] = None + self._packages_to_file_hash: Dict[str, str] = {} + self._all_packages: Dict[str, Dict[str, str]] = {} + self._inflight_package_req: Optional[str] = None + self._last_polling: Optional[float] = None + self._invalid_request = False + self._async_result: Optional[Future] = None + + def setup(self) -> None: + """Implement the setup.""" + self.context.logger.info("Setting up IpfsPackageDownloader") + self._packages_to_file_hash = { + value: key + for key, values in self.params.file_hash_to_id.items() + for value in values + } + + def act(self) -> None: + """Implement the act.""" + self._download_packages() + + @property + def params(self) -> Params: + """Get the parameters.""" + return cast(Params, self.context.params) + + @property + def request_id_to_num_timeouts(self) -> Dict[int, int]: + """Maps the request id to the number of times it has timed out.""" + return self.params.request_id_to_num_timeouts + + def count_timeout(self, request_id: int) -> None: + """Increase the timeout for a request.""" + self.request_id_to_num_timeouts[request_id] += 1 + + def timeout_limit_reached(self, request_id: int) -> bool: + """Check if the timeout limit has been reached.""" + return self.params.timeout_limit <= self.request_id_to_num_timeouts[request_id] + + def _has_executing_task_timed_out(self) -> bool: + """Check if the executing task timed out.""" + if self._executing_task is None: + return False + timeout_deadline = self._executing_task.get("timeout_deadline", None) + if timeout_deadline is None: + return False + return timeout_deadline <= time.time() + + def _download_packages(self) -> None: + """Download packages.""" + if self._inflight_package_req is not None: + # there already is a req in flight + return + if len(self._packages_to_file_hash) == len(self._all_packages): + # we already have all the packages + return + for package, file_hash in self._packages_to_file_hash.items(): + if package in self._all_packages: + continue + # read one at a time + ipfs_msg, message = self._build_ipfs_get_file_req(file_hash) + self._inflight_package_req = package + self.send_message(ipfs_msg, message, self._handle_get_package) + return + + def load_custom_component( + self, serialized_objects: Dict[str, str] + ) -> Dict[str, Any]: + """Load a custom component package. + + :param serialized_objects: the serialized objects. + :return: the component.yaml, entry_point.py and callable as tuple. + """ + # the package MUST contain a component.yaml file + if self.params.component_yaml_filename not in serialized_objects: + self.context.logger.error( + "Invalid component package. " + f"The package MUST contain a {self.params.component_yaml_filename}." + ) + return {} + # load the component.yaml file + component_yaml = yaml.safe_load( + serialized_objects[self.params.component_yaml_filename] + ) + if self.params.entry_point_key not in component_yaml or not all( + callable_key in component_yaml for callable_key in self.params.callable_keys + ): + self.context.logger.error( + f"Invalid component package. The {self.params.component_yaml_filename} file MUST contain the " + f"{self.params.entry_point_key} and {self.params.callable_keys} keys." + ) + return {} + # the name of the script that needs to be executed + entry_point_name = component_yaml[self.params.entry_point_key] + # load the script + if entry_point_name not in serialized_objects: + self.context.logger.error( + f"Invalid component package. " + f"The entry point {entry_point_name!r} is not present in the component package." + ) + return {} + entry_point = serialized_objects[entry_point_name] + # initialize with the methods that need to be called + component = { + callable_key: component_yaml[callable_key] + for callable_key in self.params.callable_keys + } + component.update( + { + COMPONENT_YAML_STORE_KEY: component_yaml, + ENTRY_POINT_STORE_KEY: entry_point, + } + ) + return component + + def _handle_get_package(self, message: IpfsMessage, _dialogue: Dialogue) -> None: + """Handle get package response""" + package_req = cast(str, self._inflight_package_req) + self._all_packages[package_req] = message.files + self.context.shared_state[package_req] = self.load_custom_component( + message.files + ) + self._inflight_package_req = None + + def send_message( + self, msg: Message, dialogue: Dialogue, callback: Callable + ) -> None: + """Send message.""" + self.context.outbox.put_message(message=msg) + nonce = dialogue.dialogue_label.dialogue_reference[0] + self.params.req_to_callback[nonce] = callback + self.params.in_flight_req = True + + def _build_ipfs_message( + self, + performative: IpfsMessage.Performative, + timeout: Optional[float] = None, + **kwargs: Any, + ) -> Tuple[IpfsMessage, IpfsDialogue]: + """Builds an IPFS message.""" + ipfs_dialogues = cast(IpfsDialogues, self.context.ipfs_dialogues) + message, dialogue = ipfs_dialogues.create( + counterparty=str(IPFS_CONNECTION_ID), + performative=performative, + timeout=timeout, + **kwargs, + ) + return message, dialogue + + def _build_ipfs_get_file_req( + self, + ipfs_hash: str, + timeout: Optional[float] = None, + ) -> Tuple[IpfsMessage, IpfsDialogue]: + """ + Builds a GET_FILES IPFS request. + + :param ipfs_hash: the ipfs hash of the file/dir to download. + :param timeout: timeout for the request. + :returns: the ipfs message, and its corresponding dialogue. + """ + message, dialogue = self._build_ipfs_message( + performative=IpfsMessage.Performative.GET_FILES, # type: ignore + ipfs_hash=ipfs_hash, + timeout=timeout, + ) + return message, dialogue diff --git a/packages/valory/skills/ipfs_package_downloader/dialogues.py b/packages/valory/skills/ipfs_package_downloader/dialogues.py new file mode 100644 index 0000000..884512c --- /dev/null +++ b/packages/valory/skills/ipfs_package_downloader/dialogues.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""This module contains dialogues.""" + +from typing import Any + +from aea.common import Address +from aea.protocols.base import Message +from aea.protocols.dialogue.base import Dialogue as BaseDialogue +from aea.skills.base import Model + +from packages.valory.protocols.ipfs.dialogues import IpfsDialogue as BaseIpfsDialogue +from packages.valory.protocols.ipfs.dialogues import IpfsDialogues as BaseIpfsDialogues + + +IpfsDialogue = BaseIpfsDialogue + + +class IpfsDialogues(Model, BaseIpfsDialogues): + """A class to keep track of IPFS dialogues.""" + + def __init__(self, **kwargs: Any) -> None: + """ + Initialize dialogues. + + :param kwargs: keyword arguments + """ + Model.__init__(self, **kwargs) + + def role_from_first_message( # pylint: disable=unused-argument + message: Message, receiver_address: Address + ) -> BaseDialogue.Role: + """Infer the role of the agent from an incoming/outgoing first message + + :param message: an incoming/outgoing first message + :param receiver_address: the address of the receiving agent + :return: The role of the agent + """ + return IpfsDialogue.Role.SKILL + + BaseIpfsDialogues.__init__( + self, + self_address=str(self.skill_id), + role_from_first_message=role_from_first_message, + ) diff --git a/packages/valory/skills/ipfs_package_downloader/handlers.py b/packages/valory/skills/ipfs_package_downloader/handlers.py new file mode 100644 index 0000000..1cad489 --- /dev/null +++ b/packages/valory/skills/ipfs_package_downloader/handlers.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains a scaffold of a handler.""" + +from typing import cast + +from aea.protocols.base import Message +from aea.skills.base import Handler + +from packages.valory.protocols.ipfs import IpfsMessage +from packages.valory.skills.ipfs_package_downloader.models import Params + + +class BaseHandler(Handler): + """Base Handler""" + + def setup(self) -> None: + """Set up the handler.""" + self.context.logger.info(f"{self.__class__.__name__}: setup method called.") + + def cleanup_dialogues(self) -> None: + """Clean up all dialogues.""" + for handler_name in self.context.handlers.__dict__.keys(): + dialogues_name = handler_name.replace("_handler", "_dialogues") + dialogues = getattr(self.context, dialogues_name) + dialogues.cleanup() + + @property + def params(self) -> Params: + """Get the parameters.""" + return cast(Params, self.context.params) + + def teardown(self) -> None: + """Teardown the handler.""" + self.context.logger.info(f"{self.__class__.__name__}: teardown called.") + + def on_message_handled(self, _message: Message) -> None: + """Callback after a message has been handled.""" + self.params.request_count += 1 + if self.params.request_count % self.params.cleanup_freq == 0: + self.context.logger.info( + f"{self.params.request_count} requests processed. Cleaning up dialogues." + ) + self.cleanup_dialogues() + + +class IpfsHandler(BaseHandler): + """IPFS API message handler.""" + + SUPPORTED_PROTOCOL = IpfsMessage.protocol_id + + def handle(self, message: Message) -> None: + """ + Implement the reaction to an IPFS message. + + :param message: the message + """ + self.context.logger.info(f"Received message: {message}") + ipfs_msg = cast(IpfsMessage, message) + if ipfs_msg.performative == IpfsMessage.Performative.ERROR: + self.context.logger.warning( + f"IPFS Message performative not recognized: {ipfs_msg.performative}" + ) + self.params.in_flight_req = False + return + + dialogue = self.context.ipfs_dialogues.update(ipfs_msg) + nonce = dialogue.dialogue_label.dialogue_reference[0] + callback = self.params.req_to_callback.pop(nonce) + callback(ipfs_msg, dialogue) + self.params.in_flight_req = False + self.on_message_handled(message) diff --git a/packages/valory/skills/ipfs_package_downloader/models.py b/packages/valory/skills/ipfs_package_downloader/models.py new file mode 100644 index 0000000..c445dbb --- /dev/null +++ b/packages/valory/skills/ipfs_package_downloader/models.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the shared state for the skill.""" +from collections import defaultdict +from typing import Any, Callable, Dict, List, cast + +from aea.exceptions import enforce +from aea.skills.base import Model + + +class Params(Model): + """A model to represent params for multiple abci apps.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize the parameters object.""" + + self.in_flight_req: bool = False + self.req_to_callback: Dict[str, Callable] = {} + self.file_hash_to_id: Dict[ + str, List[str] + ] = self._nested_list_todict_workaround( + kwargs, + "file_hash_to_id", + ) + self.request_count: int = 0 + self.cleanup_freq = kwargs.get("cleanup_freq", 50) + self.timeout_limit = kwargs.get("timeout_limit", None) + self.component_yaml_filename = kwargs.get("component_yaml_filename", None) + self.entry_point_key = kwargs.get("entry_point_key", None) + self.callable_keys = kwargs.get("callable_keys", None) + enforce(self.timeout_limit is not None, "'timeout_limit' must be set!") + enforce( + self.component_yaml_filename is not None, + "'component_yaml_filename' must be set!", + ) + enforce(self.entry_point_key is not None, "'entry_point_key' must be set!") + enforce(self.callable_keys is not None, "'callable_keys' must be set!") + # maps the request id to the number of times it has timed out + self.request_id_to_num_timeouts: Dict[int, int] = defaultdict(lambda: 0) + super().__init__(*args, **kwargs) + + def _nested_list_todict_workaround( + self, + kwargs: Dict, + key: str, + ) -> Dict: + """Get a nested list from the kwargs and convert it to a dictionary.""" + values = cast(List, kwargs.get(key)) + if len(values) == 0: + raise ValueError(f"No {key} specified!") + return {value[0]: value[1] for value in values} diff --git a/packages/valory/skills/ipfs_package_downloader/skill.yaml b/packages/valory/skills/ipfs_package_downloader/skill.yaml new file mode 100644 index 0000000..d68da28 --- /dev/null +++ b/packages/valory/skills/ipfs_package_downloader/skill.yaml @@ -0,0 +1,57 @@ +name: ipfs_package_downloader +author: valory +version: 0.1.0 +type: skill +description: A skill used for monitoring and executing tasks. +license: Apache-2.0 +aea_version: '>=1.0.0, <2.0.0' +fingerprint: + __init__.py: bafybeiaqd27il3osn4novyzipdtql4p36mnrskk2dqdsqthgjnb4gqixii + behaviours.py: bafybeig3lvx2viwoqynctj4wfusgx6h2qd7xchp6bfbfgnxq7xolm3mdhe + dialogues.py: bafybeibdxvhpzzqn4qa7us6cpkmtliwl7qziodt2srnicrdamfcev2mumi + handlers.py: bafybeieuf3jxlnzfs4yifwuvklzc2c7ps7kf4bmnzbd54ofz3q7jhpfgqy + models.py: bafybeibqo32ohd6o6au5lspvbnvkyptctegq6gto5jgz4mk6rj673fghf4 + utils/__init__.py: bafybeige5xuo32v4dykt6shggs452eliha5e5qzci5dwr6uowash3fq23q + utils/ipfs.py: bafybeihjb237abhcjupmmyswvfk7xmzgx3d4iufixes7v3yjmlycnca4xm + utils/task.py: bafybeiggyjn23wtzahdvq447jjwzmwn3hs2bc4sxkiy3wuzkugp35uoysq +fingerprint_ignore_patterns: [] +connections: +- valory/ipfs:0.1.0:bafybeiefkqvh5ylbk77xylcmshyuafmiecopt4gvardnubq52psvogis6a +contracts: [] +protocols: +- valory/ipfs:0.1.0:bafybeiftxi2qhreewgsc5wevogi7yc5g6hbcbo4uiuaibauhv3nhfcdtvm +skills: [] +behaviours: + ipfs_package_downloader: + args: {} + class_name: IpfsPackageDownloader +handlers: + ipfs_handler: + args: {} + class_name: IpfsHandler +models: + ipfs_dialogues: + args: {} + class_name: IpfsDialogues + params: + args: + cleanup_freq: 50 + timeout_limit: 3 + file_hash_to_id: + - - bafybeiabkbfjjakf7gvewxk5gvyybqvecy3k2tipvntjcovng2sbt3dq5m + - - follow_trend_strategy + component_yaml_filename: component.yaml + entry_point_key: entry_point + callable_keys: + - run_callable + - transform_callable + - evaluate_callable + class_name: Params +dependencies: + py-multibase: + version: ==1.0.3 + py-multicodec: + version: ==0.2.1 + pyyaml: + version: <=6.0.1,>=3.10 +is_abstract: false diff --git a/packages/valory/skills/ipfs_package_downloader/utils/__init__.py b/packages/valory/skills/ipfs_package_downloader/utils/__init__.py new file mode 100644 index 0000000..0475359 --- /dev/null +++ b/packages/valory/skills/ipfs_package_downloader/utils/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""This module contains helper classes.""" diff --git a/packages/valory/skills/ipfs_package_downloader/utils/ipfs.py b/packages/valory/skills/ipfs_package_downloader/utils/ipfs.py new file mode 100644 index 0000000..5ccb20c --- /dev/null +++ b/packages/valory/skills/ipfs_package_downloader/utils/ipfs.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""This module contains helpers for IPFS interaction.""" +from typing import Any, Dict, Tuple + +import yaml +from aea.helpers.cid import CID +from multibase import multibase +from multicodec import multicodec + + +CID_PREFIX = "f01701220" + + +def get_ipfs_file_hash(data: bytes) -> str: + """Get hash from bytes""" + try: + return str(CID.from_string(data.decode())) + except Exception: # noqa + # if something goes wrong, fallback to sha256 + file_hash = data.hex() + file_hash = CID_PREFIX + file_hash + file_hash = str(CID.from_string(file_hash)) + return file_hash + + +def to_multihash(hash_string: str) -> bytes: + """To multihash string.""" + # Decode the Base32 CID to bytes + cid_bytes = multibase.decode(hash_string) + # Remove the multicodec prefix (0x01) from the bytes + multihash_bytes = multicodec.remove_prefix(cid_bytes) + # Convert the multihash bytes to a hexadecimal string + hex_multihash = multihash_bytes.hex() + return hex_multihash[6:] + + +class ComponentPackageLoader: + """Component package loader.""" + + @staticmethod + def load(serialized_objects: Dict[str, str]) -> Tuple[Dict[str, Any], str, str]: + """ + Load a custom component package. + + :param serialized_objects: the serialized objects. + :return: the component.yaml, entry_point.py and callable as tuple. + """ + # the package MUST contain a component.yaml file + if "component.yaml" not in serialized_objects: + raise ValueError( + "Invalid component package. " + "The package MUST contain a component.yaml." + ) + + # load the component.yaml file + component_yaml = yaml.safe_load(serialized_objects["component.yaml"]) + if "entry_point" not in component_yaml or "callable" not in component_yaml: + raise ValueError( + "Invalid component package. " + "The component.yaml file MUST contain the 'entry_point' and 'callable' keys." + ) + + # the name of the script that needs to be executed + entry_point_name = component_yaml["entry_point"] + + # load the script + if entry_point_name not in serialized_objects: + raise ValueError( + f"Invalid component package. " + f"{entry_point_name} is not present in the component package." + ) + entry_point = serialized_objects[entry_point_name] + + # the method that needs to be called + callable_method = component_yaml["callable"] + + return component_yaml, entry_point, callable_method diff --git a/packages/valory/skills/ipfs_package_downloader/utils/task.py b/packages/valory/skills/ipfs_package_downloader/utils/task.py new file mode 100644 index 0000000..8169a9c --- /dev/null +++ b/packages/valory/skills/ipfs_package_downloader/utils/task.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains a custom Loader for the ipfs connection.""" + +from typing import Any + + +class AnyToolAsTask: + """AnyToolAsTask""" + + def execute(self, *args: Any, **kwargs: Any) -> Any: + """Execute the task.""" + tool_py = kwargs.pop("tool_py") + callable_method = kwargs.pop("callable_method") + if callable_method in globals(): + del globals()[callable_method] + exec(tool_py, globals()) # pylint: disable=W0122 # nosec + method = globals()[callable_method] + return method(*args, **kwargs) diff --git a/packages/valory/skills/market_data_fetcher_abci/__init__.py b/packages/valory/skills/market_data_fetcher_abci/__init__.py new file mode 100644 index 0000000..695275f --- /dev/null +++ b/packages/valory/skills/market_data_fetcher_abci/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the implementation of the default skill.""" + +from aea.configurations.base import PublicId + + +PUBLIC_ID = PublicId.from_str("valory/market_data_fetcher_abci:0.1.0") diff --git a/packages/valory/skills/market_data_fetcher_abci/behaviours.py b/packages/valory/skills/market_data_fetcher_abci/behaviours.py new file mode 100644 index 0000000..c382ad0 --- /dev/null +++ b/packages/valory/skills/market_data_fetcher_abci/behaviours.py @@ -0,0 +1,449 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains round behaviours of MarketDataFetcherAbciApp.""" + +import json +import os +from abc import ABC +from datetime import datetime, timedelta +from typing import ( + Any, + Callable, + Dict, + Generator, + Optional, + Set, + Tuple, + Type, + Union, + cast, +) + +from packages.eightballer.connections.dcxt.connection import ( + PUBLIC_ID as DCXT_CONNECTION_ID, +) +from packages.eightballer.protocols.tickers.message import TickersMessage +from packages.valory.skills.abstract_round_abci.base import AbstractRound +from packages.valory.skills.abstract_round_abci.behaviour_utils import SOLANA_LEDGER_ID +from packages.valory.skills.abstract_round_abci.behaviours import ( + AbstractRoundBehaviour, + BaseBehaviour, +) +from packages.valory.skills.abstract_round_abci.io_.store import SupportedFiletype +from packages.valory.skills.market_data_fetcher_abci.models import Coingecko, Params +from packages.valory.skills.market_data_fetcher_abci.payloads import ( + TransformedMarketDataPayload, +) +from packages.valory.skills.market_data_fetcher_abci.rounds import ( + FetchMarketDataRound, + MarketDataFetcherAbciApp, + MarketDataPayload, + SynchronizedData, + TransformMarketDataRound, +) + + +HTTP_OK = [200, 201] +MAX_RETRIES = 3 +MARKETS_FILE_NAME = "markets.json" +TOKEN_ID_FIELD = "coingecko_id" # nosec: B105:hardcoded_password_string +TOKEN_ADDRESS_FIELD = "address" # nosec: B105:hardcoded_password_string +UTF8 = "utf-8" +STRATEGY_KEY = "trading_strategy" +ENTRY_POINT_STORE_KEY = "entry_point" +TRANSFORM_CALLABLE_STORE_KEY = "transform_callable" +DEFAULT_MARKET_SEPARATOR = "/" +DEFAULT_DATE_RANGE_SECONDS = 300 +DEFAULT_DATA_LOOKBACK_MINUTES = 60 +DEFAULT_DATA_VOLUME = 100 +SECONDS_TO_MILLISECONDS = 1000 + + +def date_range_generator( # type: ignore + start, end, seconds_delta=DEFAULT_DATE_RANGE_SECONDS +) -> Generator[int, None, None]: + """Generate a range of dates in the ms format. We do this as tickers are just spot prices and we dont yet retrieve historical data.""" + current = start + while current < end: + yield current.timestamp() * SECONDS_TO_MILLISECONDS + current += timedelta(seconds=seconds_delta) + + +class MarketDataFetcherBaseBehaviour(BaseBehaviour, ABC): + """Base behaviour for the market_data_fetcher_abci skill.""" + + @property + def synchronized_data(self) -> SynchronizedData: + """Return the synchronized data.""" + return cast(SynchronizedData, super().synchronized_data) + + @property + def params(self) -> Params: + """Return the params.""" + return cast(Params, super().params) + + @property + def coingecko(self) -> Coingecko: + """Return the Coingecko.""" + return cast(Coingecko, self.context.coingecko) + + def from_data_dir(self, path: str) -> str: + """Return the given path appended to the data dir.""" + return os.path.join(self.context.data_dir, path) + + def _request_with_retries( + self, + endpoint: str, + rate_limited_callback: Callable, + method: str = "GET", + body: Optional[Any] = None, + headers: Optional[Dict] = None, + rate_limited_code: int = 429, + max_retries: int = MAX_RETRIES, + retry_wait: int = 0, + ) -> Generator[None, None, Tuple[bool, Dict]]: + """Request wrapped around a retry mechanism""" + + self.context.logger.info(f"HTTP {method} call: {endpoint}") + content = json.dumps(body).encode(UTF8) if body else None + + retries = 0 + while True: + # Make the request + response = yield from self.get_http_response( + method, endpoint, content, headers + ) + + try: + response_json = json.loads(response.body) + except json.decoder.JSONDecodeError as exc: + self.context.logger.error(f"Exception during json loading: {exc}") + response_json = {"exception": str(exc)} + + if response.status_code == rate_limited_code: + rate_limited_callback() + return False, response_json + + if response.status_code not in HTTP_OK or "exception" in response_json: + self.context.logger.error( + f"Request failed [{response.status_code}]: {response_json}" + ) + retries += 1 + if retries == max_retries: + break + yield from self.sleep(retry_wait) + continue + + self.context.logger.info("Request succeeded.") + return True, response_json + + self.context.logger.error(f"Request failed after {retries} retries.") + return False, response_json + + def get_dcxt_response( + self, + protocol_performative: TickersMessage.Performative, + **kwargs: Any, + ) -> Generator[None, None, Any]: + """Get a ccxt response.""" + if protocol_performative not in self._performative_to_dialogue_class: + raise ValueError( + f"Unsupported protocol performative {protocol_performative:!r}" + ) + dialogue_class = self._performative_to_dialogue_class[protocol_performative] + + msg, dialogue = dialogue_class.create( + counterparty=str(DCXT_CONNECTION_ID), + performative=protocol_performative, + **kwargs, + ) + msg._sender = str(self.context.skill_id) # pylint: disable=protected-access + response = yield from self._do_request(msg, dialogue) + return response + + def __init__(self, **kwargs: Any): + """Initialize the behaviour.""" + super().__init__(**kwargs) + self._performative_to_dialogue_class = { + TickersMessage.Performative.GET_ALL_TICKERS: self.context.tickers_dialogues, + } + + +class FetchMarketDataBehaviour(MarketDataFetcherBaseBehaviour): + """FetchMarketDataBehaviour""" + + matching_round: Type[AbstractRound] = FetchMarketDataRound + + exchange_to_tickers: Dict[str, Any] = {} + + def async_act(self) -> Generator: + """Do the act, supporting asynchronous execution.""" + + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + data_hash = yield from self.fetch_markets() + sender = self.context.agent_address + payload = MarketDataPayload(sender=sender, data_hash=data_hash) + + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + yield from self.send_a2a_transaction(payload) + yield from self.wait_until_round_end() + + self.set_done() + + def _fetch_solana_market_data( + self, + ) -> Generator[None, None, Dict[Any, Dict[str, Any]]]: + """Fetch Solana market data from Coingecko and send to IPFS""" + + headers = { + "Accept": "application/json", + } + if self.coingecko.api_key: + headers["x-cg-pro-api-key"] = self.coingecko.api_key + markets = {} + for token_data in self.params.token_symbol_whitelist: + token_id = token_data.get(TOKEN_ID_FIELD, None) + token_address = token_data.get(TOKEN_ADDRESS_FIELD, None) + + if not token_id or not token_address: + err = f"Token id or address missing in whitelist's {token_data=}." + self.context.logger.error(err) + continue + + warned = False + while not self.coingecko.rate_limiter.check_and_burn(): + if not warned: + self.context.logger.warning( + "Rate limiter activated. " + "To avoid this in the future, you may consider acquiring a Coingecko API key," + "and updating the `Coingecko` model's overrides.\n" + "Cooling down..." + ) + warned = True + yield from self.sleep(self.params.sleep_time) + + if warned: + self.context.logger.info("Cooldown period passed :)") + + remaining_limit = self.coingecko.rate_limiter.remaining_limit + remaining_credits = self.coingecko.rate_limiter.remaining_credits + self.context.logger.info( + "Local rate limiter's check passed. " + f"After the call, you will have {remaining_limit=} and {remaining_credits=}." + ) + success, response_json = yield from self._request_with_retries( + endpoint=self.coingecko.endpoint.format(token_id=token_id), + headers=headers, + rate_limited_code=self.coingecko.rate_limited_code, + rate_limited_callback=self.coingecko.rate_limited_status_callback, + retry_wait=self.params.sleep_time, + ) + + # Skip failed markets. The strategy will need to verify market availability + if not success: + self.context.logger.error( + f"Failed to fetch market data for {token_id}." + ) + continue + + self.context.logger.info( + f"Successfully fetched market data for {token_id}." + ) + # we collect a tuple of the prices and the volumes + + prices = response_json.get(self.coingecko.prices_field, []) + volumes = response_json.get(self.coingecko.volumes_field, []) + prices_volumes = {"prices": prices, "volumes": volumes} + markets[token_address] = prices_volumes + return markets + + def _fetch_dcxt_market_data( + self, ledger_id: str + ) -> Generator[None, None, Dict[Union[str, Any], Dict[str, object]]]: + params = { + "ledger_id": ledger_id, + } + for key, value in params.items(): + params[key] = value.encode("utf-8") # type: ignore + exchanges = self.params.exchange_ids[ledger_id] + + markets = {} + for exchange_id in exchanges: + msg: TickersMessage = yield from self.get_dcxt_response( + protocol_performative=TickersMessage.Performative.GET_ALL_TICKERS, # type: ignore + exchange_id=f"{exchange_id}_{ledger_id}", + params=params, + ) + self.context.logger.info( + f"Received {len(msg.tickers.tickers)} tickers from {exchange_id}" + ) + + for ticker in msg.tickers.tickers: + token_address = ticker.symbol.split(DEFAULT_MARKET_SEPARATOR)[0] # type: ignore + + dates = list( + date_range_generator( + datetime.now() + - timedelta(minutes=DEFAULT_DATA_LOOKBACK_MINUTES), + datetime.now(), + ) + ) + prices = [[date, ticker.ask] for date in dates] + volumes = [ + [ + date, + DEFAULT_DATA_VOLUME, # This is a placeholder for the volume + ] + for date in dates + ] + prices_volumes = {"prices": prices, "volumes": volumes} + markets[token_address] = prices_volumes + return markets + + def fetch_markets(self) -> Generator[None, None, Optional[str]]: + """Fetch markets from Coingecko and send to IPFS""" + + ledger_market_data: Dict[str, Dict[str, Any]] = {} + + # Get the market data for each token for each ledger_id + for ledger_id in self.params.ledger_ids: + self.context.logger.info(f"Fetching market data for {ledger_id}.") + if ledger_id == SOLANA_LEDGER_ID: + markets = yield from self._fetch_solana_market_data() + ledger_market_data.update(SOLANA_LEDGER_ID=markets) + else: + # We assume it is an EVM chain and thus route to dcxt. + markets = yield from self._fetch_dcxt_market_data(ledger_id) + ledger_market_data.update({ledger_id: markets}) + data_hash = None + if markets: + data_hash = yield from self.send_to_ipfs( + filename=self.from_data_dir(MARKETS_FILE_NAME), + obj=markets, + filetype=SupportedFiletype.JSON, + ) + self.context.logger.info( + f"Market file stored in IPFS. Hash is {data_hash}." + ) + + return data_hash + + +class TransformMarketDataBehaviour(MarketDataFetcherBaseBehaviour): + """Behaviour to transform the fetched signals.""" + + matching_round: Type[AbstractRound] = TransformMarketDataRound + + def strategy_store(self, strategy_name: str) -> Dict[str, str]: + """Get the stored strategy's files.""" + return self.context.shared_state.get(strategy_name, {}) + + def execute_strategy_transformation( + self, *args: Any, **kwargs: Any + ) -> Dict[str, Any] | None: + """Execute the strategy's transform method and return the results.""" + trading_strategy = kwargs.pop(STRATEGY_KEY, None) + if trading_strategy is None: + self.context.logger.error(f"No {STRATEGY_KEY!r} was given!") + return None + + store = self.strategy_store(trading_strategy) + strategy_exec = store.get(ENTRY_POINT_STORE_KEY, None) + if strategy_exec is None: + self.context.logger.error( + f"No executable was found for {trading_strategy=}! Did the IPFS package downloader load it correctly?" + ) + return None + + callable_method = store.get(TRANSFORM_CALLABLE_STORE_KEY, None) + if callable_method is None: + self.context.logger.error( + "No transform callable was found in the loaded component! " + "Did the IPFS package downloader load it correctly?" + ) + return None + + if callable_method in globals(): + del globals()[callable_method] + + exec(strategy_exec, globals()) # pylint: disable=W0122 # nosec + method = globals().get(callable_method, None) + if method is None: + self.context.logger.error( + f"No {callable_method!r} method was found in {trading_strategy} strategy's executable:\n" + f"{strategy_exec}." + ) + return None + return method(*args, **kwargs) + + def async_act(self) -> Generator: + """Do the act, supporting asynchronous execution.""" + + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + data_hash = yield from self.transform_data() + sender = self.context.agent_address + payload = TransformedMarketDataPayload( + sender=sender, transformed_data_hash=data_hash + ) + + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + yield from self.send_a2a_transaction(payload) + yield from self.wait_until_round_end() + + self.set_done() + + def transform_data( + self, + ) -> Generator[None, None, Optional[str]]: + """Transform the data to OHLCV format.""" + markets_data = yield from self.get_from_ipfs( + self.synchronized_data.data_hash, SupportedFiletype.JSON + ) + markets_data = cast(Dict[str, Dict[str, Any]], markets_data) + results = {} + + strategy = self.synchronized_data.selected_strategy + for token_address, market_data in markets_data.items(): + kwargs = {STRATEGY_KEY: strategy, **market_data} + result = self.execute_strategy_transformation(**kwargs) + if result is None: + self.context.logger.error( + f"Failed to transform market data for {token_address}." + ) + continue + results[token_address] = result + + data_hash = yield from self.send_to_ipfs( + filename=self.from_data_dir(MARKETS_FILE_NAME), + obj=results, + filetype=SupportedFiletype.JSON, + ) + return data_hash + + +class MarketDataFetcherRoundBehaviour(AbstractRoundBehaviour): + """MarketDataFetcherRoundBehaviour""" + + initial_behaviour_cls = FetchMarketDataBehaviour + abci_app_cls = MarketDataFetcherAbciApp + behaviours: Set[Type[BaseBehaviour]] = { + FetchMarketDataBehaviour, # type: ignore + TransformMarketDataBehaviour, # type: ignore + } diff --git a/packages/valory/skills/market_data_fetcher_abci/dialogues.py b/packages/valory/skills/market_data_fetcher_abci/dialogues.py new file mode 100644 index 0000000..ba3bcbd --- /dev/null +++ b/packages/valory/skills/market_data_fetcher_abci/dialogues.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the dialogues of the MarketDataFetcherAbciApp.""" + +from packages.eightballer.protocols.tickers.dialogues import ( + TickersDialogue as BaseTickersDialogue, +) +from packages.eightballer.protocols.tickers.dialogues import ( + TickersDialogues as BaseTickersDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + AbciDialogue as BaseAbciDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + AbciDialogues as BaseAbciDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + ContractApiDialogue as BaseContractApiDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + ContractApiDialogues as BaseContractApiDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + HttpDialogue as BaseHttpDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + HttpDialogues as BaseHttpDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + IpfsDialogue as BaseIpfsDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + IpfsDialogues as BaseIpfsDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + LedgerApiDialogue as BaseLedgerApiDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + LedgerApiDialogues as BaseLedgerApiDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + SigningDialogue as BaseSigningDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + SigningDialogues as BaseSigningDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + TendermintDialogue as BaseTendermintDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + TendermintDialogues as BaseTendermintDialogues, +) + + +AbciDialogue = BaseAbciDialogue +AbciDialogues = BaseAbciDialogues + + +HttpDialogue = BaseHttpDialogue +HttpDialogues = BaseHttpDialogues + + +SigningDialogue = BaseSigningDialogue +SigningDialogues = BaseSigningDialogues + + +LedgerApiDialogue = BaseLedgerApiDialogue +LedgerApiDialogues = BaseLedgerApiDialogues + + +ContractApiDialogue = BaseContractApiDialogue +ContractApiDialogues = BaseContractApiDialogues + + +TendermintDialogue = BaseTendermintDialogue +TendermintDialogues = BaseTendermintDialogues + + +IpfsDialogue = BaseIpfsDialogue +IpfsDialogues = BaseIpfsDialogues + + +TickersDialogue = BaseTickersDialogue +TickersDialogues = BaseTickersDialogues diff --git a/packages/valory/skills/market_data_fetcher_abci/fsm_specification.yaml b/packages/valory/skills/market_data_fetcher_abci/fsm_specification.yaml new file mode 100644 index 0000000..b7d5fba --- /dev/null +++ b/packages/valory/skills/market_data_fetcher_abci/fsm_specification.yaml @@ -0,0 +1,26 @@ +alphabet_in: +- DONE +- NONE +- NO_MAJORITY +- ROUND_TIMEOUT +default_start_state: FetchMarketDataRound +final_states: +- FailedMarketFetchRound +- FinishedMarketFetchRound +label: MarketDataFetcherAbciApp +start_states: +- FetchMarketDataRound +states: +- FailedMarketFetchRound +- FetchMarketDataRound +- FinishedMarketFetchRound +- TransformMarketDataRound +transition_func: + (FetchMarketDataRound, DONE): TransformMarketDataRound + (FetchMarketDataRound, NONE): FailedMarketFetchRound + (FetchMarketDataRound, NO_MAJORITY): FetchMarketDataRound + (FetchMarketDataRound, ROUND_TIMEOUT): FetchMarketDataRound + (TransformMarketDataRound, DONE): FinishedMarketFetchRound + (TransformMarketDataRound, NONE): FailedMarketFetchRound + (TransformMarketDataRound, NO_MAJORITY): TransformMarketDataRound + (TransformMarketDataRound, ROUND_TIMEOUT): TransformMarketDataRound diff --git a/packages/valory/skills/market_data_fetcher_abci/handlers.py b/packages/valory/skills/market_data_fetcher_abci/handlers.py new file mode 100644 index 0000000..2ae35dd --- /dev/null +++ b/packages/valory/skills/market_data_fetcher_abci/handlers.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the handlers for the skill of MarketDataFetcherAbciApp.""" + +from packages.eightballer.protocols.tickers.message import TickersMessage +from packages.valory.skills.abstract_round_abci.handlers import ( + ABCIRoundHandler as BaseABCIRoundHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import AbstractResponseHandler +from packages.valory.skills.abstract_round_abci.handlers import ( + ContractApiHandler as BaseContractApiHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + HttpHandler as BaseHttpHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + IpfsHandler as BaseIpfsHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + LedgerApiHandler as BaseLedgerApiHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + SigningHandler as BaseSigningHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + TendermintHandler as BaseTendermintHandler, +) + + +class DcxtTickersHandler(AbstractResponseHandler): + """This class implements a handler for DexTickersHandler messages.""" + + SUPPORTED_PROTOCOL = TickersMessage.protocol_id + allowed_response_performatives = frozenset( + { + TickersMessage.Performative.ALL_TICKERS, + TickersMessage.Performative.TICKER, + TickersMessage.Performative.ERROR, + } + ) + + +ABCIHandler = BaseABCIRoundHandler +HttpHandler = BaseHttpHandler +SigningHandler = BaseSigningHandler +LedgerApiHandler = BaseLedgerApiHandler +ContractApiHandler = BaseContractApiHandler +TendermintHandler = BaseTendermintHandler +IpfsHandler = BaseIpfsHandler diff --git a/packages/valory/skills/market_data_fetcher_abci/models.py b/packages/valory/skills/market_data_fetcher_abci/models.py new file mode 100644 index 0000000..6a3ac6e --- /dev/null +++ b/packages/valory/skills/market_data_fetcher_abci/models.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the shared state for the abci skill of MarketDataFetcherAbciApp.""" + +from datetime import datetime +from time import time +from typing import Any, Dict, List, Optional + +from aea.skills.base import Model + +from packages.valory.skills.abstract_round_abci.models import BaseParams +from packages.valory.skills.abstract_round_abci.models import ( + BenchmarkTool as BaseBenchmarkTool, +) +from packages.valory.skills.abstract_round_abci.models import Requests as BaseRequests +from packages.valory.skills.abstract_round_abci.models import ( + SharedState as BaseSharedState, +) +from packages.valory.skills.abstract_round_abci.models import TypeCheckMixin +from packages.valory.skills.market_data_fetcher_abci.rounds import ( + MarketDataFetcherAbciApp, +) + + +MINUTE_UNIX = 60 + + +def format_whitelist(token_whitelist: List) -> List: + """Load the token whitelist into its proper format""" + fixed_whitelist = [] + for element in token_whitelist: + token_config = {} + for i in element.split("&"): + key, value = i.split("=") + token_config[key] = value + fixed_whitelist.append(token_config) + return fixed_whitelist + + +class SharedState(BaseSharedState): + """Keep the current shared state of the skill.""" + + abci_app_cls = MarketDataFetcherAbciApp + + +class Params(BaseParams): + """Parameters.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize the parameters object.""" + self.token_symbol_whitelist: List[Dict] = format_whitelist( + self._ensure("token_symbol_whitelist", kwargs, List[str]) + ) + self.ledger_ids = self._ensure("ledger_ids", kwargs, List[str]) + self.exchange_ids = self._ensure("exchange_ids", kwargs, Dict[str, List[str]]) + super().__init__(*args, **kwargs) + + +class CoingeckoRateLimiter: + """Keeps track of the rate limiting for Coingecko.""" + + def __init__(self, limit: int, credits_: int) -> None: + """Initialize the Coingecko rate limiter.""" + self._limit = self._remaining_limit = limit + self._credits = self._remaining_credits = credits_ + self._last_request_time = time() + + @property + def limit(self) -> int: + """Get the limit per minute.""" + return self._limit + + @property + def credits(self) -> int: + """Get the requests' cap per month.""" + return self._credits + + @property + def remaining_limit(self) -> int: + """Get the remaining limit per minute.""" + return self._remaining_limit + + @property + def remaining_credits(self) -> int: + """Get the remaining requests' cap per month.""" + return self._remaining_credits + + @property + def last_request_time(self) -> float: + """Get the timestamp of the last request.""" + return self._last_request_time + + @property + def rate_limited(self) -> bool: + """Check whether we are rate limited.""" + return self.remaining_limit == 0 + + @property + def no_credits(self) -> bool: + """Check whether all the credits have been spent.""" + return self.remaining_credits == 0 + + @property + def cannot_request(self) -> bool: + """Check whether we cannot perform a request.""" + return self.rate_limited or self.no_credits + + @property + def credits_reset_timestamp(self) -> int: + """Get the UNIX timestamp in which the Coingecko credits reset.""" + current_date = datetime.now() + first_day_of_next_month = datetime(current_date.year, current_date.month + 1, 1) + return int(first_day_of_next_month.timestamp()) + + @property + def can_reset_credits(self) -> bool: + """Check whether the Coingecko credits can be reset.""" + return self.last_request_time >= self.credits_reset_timestamp + + def _update_limits(self) -> None: + """Update the remaining limits and the credits if necessary.""" + time_passed = time() - self.last_request_time + limit_increase = int(time_passed / MINUTE_UNIX) * self.limit + self._remaining_limit = min(self.limit, self.remaining_limit + limit_increase) + if self.can_reset_credits: + self._remaining_credits = self.credits + + def _burn_credit(self) -> None: + """Use one credit.""" + self._remaining_limit -= 1 + self._remaining_credits -= 1 + self._last_request_time = time() + + def check_and_burn(self) -> bool: + """Check whether we can perform a new request, and if yes, update the remaining limit and credits.""" + self._update_limits() + if self.cannot_request: + return False + self._burn_credit() + return True + + +class Coingecko(Model, TypeCheckMixin): + """Coingecko configuration.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize the Coingecko object.""" + self.endpoint: str = self._ensure("endpoint", kwargs, str) + self.api_key: Optional[str] = self._ensure("api_key", kwargs, Optional[str]) + self.prices_field: str = self._ensure("prices_field", kwargs, str) + self.volumes_field: str = self._ensure("volumes_field", kwargs, str) + self.rate_limited_code: int = self._ensure("rate_limited_code", kwargs, int) + limit: int = self._ensure("requests_per_minute", kwargs, int) + credits_: int = self._ensure("credits", kwargs, int) + self.rate_limiter = CoingeckoRateLimiter(limit, credits_) + super().__init__(*args, **kwargs) + + def rate_limited_status_callback(self) -> None: + """Callback when a rate-limited status is returned from the API.""" + self.context.logger.error( + "Unexpected rate-limited status code was received from the Coingecko API! " + "Setting the limit to 0 on the local rate limiter to partially address the issue. " + "Please check whether the `Coingecko` overrides are set corresponding to the API's rules." + ) + self.rate_limiter._remaining_limit = 0 + self.rate_limiter._last_request_time = time() + + +Requests = BaseRequests +BenchmarkTool = BaseBenchmarkTool diff --git a/packages/valory/skills/market_data_fetcher_abci/payloads.py b/packages/valory/skills/market_data_fetcher_abci/payloads.py new file mode 100644 index 0000000..3ccd890 --- /dev/null +++ b/packages/valory/skills/market_data_fetcher_abci/payloads.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the transaction payloads of the MarketDataFetcherAbciApp.""" + +from dataclasses import dataclass +from typing import Optional + +from packages.valory.skills.abstract_round_abci.base import BaseTxPayload + + +@dataclass(frozen=True) +class MarketDataPayload(BaseTxPayload): + """Represent a transaction payload for the market data.""" + + data_hash: Optional[str] + + +@dataclass(frozen=True) +class TransformedMarketDataPayload(BaseTxPayload): + """Represent a transaction payload for the market data.""" + + transformed_data_hash: Optional[str] diff --git a/packages/valory/skills/market_data_fetcher_abci/rounds.py b/packages/valory/skills/market_data_fetcher_abci/rounds.py new file mode 100644 index 0000000..f47dbc5 --- /dev/null +++ b/packages/valory/skills/market_data_fetcher_abci/rounds.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains the rounds of MarketDataFetcherAbciApp.""" + +from enum import Enum +from typing import Dict, FrozenSet, Set, Type + +from packages.valory.skills.abstract_round_abci.base import ( + AbciApp, + AbciAppTransitionFunction, + AppState, + BaseSynchronizedData, + BaseTxPayload, + CollectSameUntilThresholdRound, + CollectionRound, + DegenerateRound, + DeserializedCollection, + EventToTimeout, + get_name, +) +from packages.valory.skills.market_data_fetcher_abci.payloads import ( + MarketDataPayload, + TransformedMarketDataPayload, +) + + +class Event(Enum): + """MarketDataFetcherAbciApp Events""" + + DONE = "done" + NONE = "none" + NO_MAJORITY = "no_majority" + ROUND_TIMEOUT = "round_timeout" + + +class SynchronizedData(BaseSynchronizedData): + """ + Class to represent the synchronized data. + + This data is replicated by the tendermint application. + """ + + def _get_deserialized(self, key: str) -> DeserializedCollection: + """Strictly get a collection and return it deserialized.""" + serialized = self.db.get_strict(key) + return CollectionRound.deserialize_collection(serialized) + + @property + def data_hash(self) -> str: + """Get the hash of the tokens' data.""" + return str(self.db.get_strict("data_hash")) + + @property + def transformed_data_hash(self) -> str: + """Get the hash of the tokens' data.""" + return str(self.db.get_strict("transformed_data_hash")) + + @property + def participant_to_fetching(self) -> DeserializedCollection: + """Get the participants to market fetching.""" + return self._get_deserialized("participant_to_fetching") + + @property + def participant_to_transforming(self) -> DeserializedCollection: + """Get the participants to market data transformation.""" + return self._get_deserialized("participant_to_transforming") + + @property + def selected_strategy(self) -> str: + """Get the selected strategy.""" + return self.db.get_strict("selected_strategy") + + +class FetchMarketDataRound(CollectSameUntilThresholdRound): + """FetchMarketDataRound""" + + payload_class: Type[BaseTxPayload] = MarketDataPayload + synchronized_data_class = SynchronizedData + done_event = Event.DONE + none_event = Event.NONE + no_majority_event = Event.NO_MAJORITY + selection_key = get_name(SynchronizedData.data_hash) + collection_key = get_name(SynchronizedData.participant_to_fetching) + + +class TransformMarketDataRound(FetchMarketDataRound): + """Round to transform the fetched signals.""" + + payload_class = TransformedMarketDataPayload + selection_key = get_name(SynchronizedData.transformed_data_hash) + collection_key = get_name(SynchronizedData.participant_to_transforming) + + +class FinishedMarketFetchRound(DegenerateRound): + """FinishedMarketFetchRound""" + + +class FailedMarketFetchRound(DegenerateRound): + """FailedMarketFetchRound""" + + +class MarketDataFetcherAbciApp(AbciApp[Event]): + """MarketDataFetcherAbciApp + + Initial round: FetchMarketDataRound + + Initial states: {FetchMarketDataRound} + + Transition states: + 0. FetchMarketDataRound + - done: 1. + - none: 3. + - no majority: 0. + - round timeout: 0. + 1. TransformMarketDataRound + - done: 2. + - none: 3. + - no majority: 1. + - round timeout: 1. + 2. FinishedMarketFetchRound + 3. FailedMarketFetchRound + + Final states: {FailedMarketFetchRound, FinishedMarketFetchRound} + + Timeouts: + round timeout: 30.0 + """ + + initial_round_cls: AppState = FetchMarketDataRound + initial_states: Set[AppState] = {FetchMarketDataRound} + transition_function: AbciAppTransitionFunction = { + FetchMarketDataRound: { + Event.DONE: TransformMarketDataRound, + Event.NONE: FailedMarketFetchRound, + Event.NO_MAJORITY: FetchMarketDataRound, + Event.ROUND_TIMEOUT: FetchMarketDataRound, + }, + TransformMarketDataRound: { + Event.DONE: FinishedMarketFetchRound, + Event.NONE: FailedMarketFetchRound, + Event.NO_MAJORITY: TransformMarketDataRound, + Event.ROUND_TIMEOUT: TransformMarketDataRound, + }, + FinishedMarketFetchRound: {}, + FailedMarketFetchRound: {}, + } + final_states: Set[AppState] = {FinishedMarketFetchRound, FailedMarketFetchRound} + event_to_timeout: EventToTimeout = { + Event.ROUND_TIMEOUT: 30.0, + } + cross_period_persisted_keys: FrozenSet[str] = frozenset() + db_pre_conditions: Dict[AppState, Set[str]] = { + FetchMarketDataRound: set(), + } + db_post_conditions: Dict[AppState, Set[str]] = { + FinishedMarketFetchRound: {get_name(SynchronizedData.data_hash)}, + FailedMarketFetchRound: set(), + } diff --git a/packages/valory/skills/market_data_fetcher_abci/skill.yaml b/packages/valory/skills/market_data_fetcher_abci/skill.yaml new file mode 100644 index 0000000..fb25ffd --- /dev/null +++ b/packages/valory/skills/market_data_fetcher_abci/skill.yaml @@ -0,0 +1,165 @@ +name: market_data_fetcher_abci +author: valory +version: 0.1.0 +type: skill +description: The scaffold skill is a scaffold for your own skill implementation. +license: Apache-2.0 +aea_version: '>=1.0.0, <2.0.0' +fingerprint: + __init__.py: bafybeih2dsi3e5ax36ooryzjkeoprkeqif3sguc4h63ldx6clkhmdsztka + behaviours.py: bafybeidbor36h5a364j2yal4idziy7u5fsofkivcizzasepliz7pxa3pki + dialogues.py: bafybeihqyvgfzkno2kch5jf7qefrwgknhraz345svglrc6bfgtmuo7gbmi + fsm_specification.yaml: bafybeib6ucxlubtfscg7vris2ia2f7iwlpzxte2bhqcvdluoge4xl2paba + handlers.py: bafybeia3jv2qaq7s6ao6b2xoyvqb4wpzc2zhu75cd26xrqjq3puqxesefy + models.py: bafybeieqo2y4yt34wrvdirmbdhxdeggtsfyc3dvwgq4thqqizbtndyzfcu + payloads.py: bafybeiaq5cqqinf34cxfn6lgefspbyhd3bcfcphqx6q2czpkall55db3vu + rounds.py: bafybeibf2cwrf6phrdnwlrfezy2erl4ux56uumc25jarqpwzhtyemtlhuu +fingerprint_ignore_patterns: [] +connections: +- eightballer/dcxt:0.1.0:bafybeihes2id6qmqqybvdqfpchu6h3i5mpnhq6lztfxp52twd22iwqxacq +contracts: [] +protocols: +- eightballer/tickers:0.1.0:bafybeicjbpa24tla2enenmlzipqhu6grutqso74q6y7is2cpk7acub3bca +skills: +- valory/abstract_round_abci:0.1.0:bafybeibovsktd3uxur45nrcomq5shcn46cgxd5idmhxbmjhg32c5abyqim +behaviours: + main: + args: {} + class_name: MarketDataFetcherRoundBehaviour +handlers: + abci: + args: {} + class_name: ABCIHandler + contract_api: + args: {} + class_name: ContractApiHandler + http: + args: {} + class_name: HttpHandler + ipfs: + args: {} + class_name: IpfsHandler + ledger_api: + args: {} + class_name: LedgerApiHandler + signing: + args: {} + class_name: SigningHandler + tendermint: + args: {} + class_name: TendermintHandler + tickers: + args: {} + class_name: DcxtTickersHandler +models: + abci_dialogues: + args: {} + class_name: AbciDialogues + benchmark_tool: + args: + log_dir: /logs + class_name: BenchmarkTool + contract_api_dialogues: + args: {} + class_name: ContractApiDialogues + http_dialogues: + args: {} + class_name: HttpDialogues + ipfs_dialogues: + args: {} + class_name: IpfsDialogues + ledger_api_dialogues: + args: {} + class_name: LedgerApiDialogues + params: + args: + cleanup_history_depth: 1 + cleanup_history_depth_current: null + drand_public_key: 868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31 + finalize_timeout: 60.0 + genesis_config: + chain_id: chain-c4daS1 + consensus_params: + block: + max_bytes: '22020096' + max_gas: '-1' + time_iota_ms: '1000' + evidence: + max_age_duration: '172800000000000' + max_age_num_blocks: '100000' + max_bytes: '1048576' + validator: + pub_key_types: + - ed25519 + version: {} + genesis_time: '2022-05-20T16:00:21.735122717Z' + voting_power: '10' + history_check_timeout: 1205 + ipfs_domain_name: null + keeper_allowed_retries: 3 + keeper_timeout: 30.0 + max_attempts: 10 + max_healthcheck: 120 + on_chain_service_id: null + request_retry_delay: 1.0 + request_timeout: 10.0 + reset_pause_duration: 10 + reset_tendermint_after: 2 + retry_attempts: 400 + retry_timeout: 3 + round_timeout_seconds: 30.0 + service_id: market_data_fetcher + service_registry_address: null + setup: + all_participants: + - '0x0000000000000000000000000000000000000000' + consensus_threshold: null + safe_contract_address: '0x0000000000000000000000000000000000000000' + share_tm_config_on_startup: false + sleep_time: 1 + tendermint_check_sleep_delay: 3 + tendermint_com_url: http://localhost:8080 + tendermint_max_retries: 5 + tendermint_p2p_url: localhost:26656 + tendermint_url: http://localhost:26657 + tx_timeout: 10.0 + validate_timeout: 1205 + token_symbol_whitelist: [] + ledger_ids: + - ethereum + exchange_ids: + ethereum: [] + optimism: + - balancer + class_name: Params + coingecko: + args: + endpoint: https://api.coingecko.com/api/v3/coins/{token_id}/market_chart?vs_currency=usd&days=1 + api_key: null + prices_field: prices + volumes_field: total_volumes + requests_per_minute: 5 + credits: 10000 + rate_limited_code: 429 + class_name: Coingecko + requests: + args: {} + class_name: Requests + signing_dialogues: + args: {} + class_name: SigningDialogues + state: + args: {} + class_name: SharedState + tendermint_dialogues: + args: {} + class_name: TendermintDialogues + tickers_dialogues: + args: {} + class_name: TickersDialogues +dependencies: + pandas: + version: '>=1.3.0' + PyYAML: + version: '>=5.4.1' +is_abstract: true diff --git a/packages/valory/skills/portfolio_tracker_abci/README.md b/packages/valory/skills/portfolio_tracker_abci/README.md new file mode 100644 index 0000000..d581827 --- /dev/null +++ b/packages/valory/skills/portfolio_tracker_abci/README.md @@ -0,0 +1,6 @@ +# Portfolio Tracker abci + +## Description + +This module contains an ABCI skill responsible for tracking the portfolio of the service, +and ensuring that there is enough balance (based on configurable input thresholds) on the agent and the multisig. diff --git a/packages/valory/skills/portfolio_tracker_abci/__init__.py b/packages/valory/skills/portfolio_tracker_abci/__init__.py new file mode 100644 index 0000000..28b6422 --- /dev/null +++ b/packages/valory/skills/portfolio_tracker_abci/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains an ABCI skill responsible for tracking the portfolio of the service.""" + +from aea.configurations.base import PublicId + + +PUBLIC_ID = PublicId.from_str("valory/portfolio_tracker_abci:0.1.0") diff --git a/packages/valory/skills/portfolio_tracker_abci/behaviours.py b/packages/valory/skills/portfolio_tracker_abci/behaviours.py new file mode 100644 index 0000000..e7ffd18 --- /dev/null +++ b/packages/valory/skills/portfolio_tracker_abci/behaviours.py @@ -0,0 +1,545 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains round behaviours of `PortfolioTrackerRoundBehaviour`.""" + +import json +from copy import deepcopy +from dataclasses import asdict +from pathlib import Path +from typing import Any, Dict, Generator, Optional, Set, Tuple, Type, cast + +from aea.configurations.constants import _SOLANA_IDENTIFIER + +from packages.eightballer.connections.dcxt.connection import ( + PUBLIC_ID as DCXT_CONNECTION_ID, +) +from packages.eightballer.protocols.balances.message import BalancesMessage +from packages.valory.protocols.ledger_api.custom_types import Kwargs +from packages.valory.protocols.ledger_api.message import LedgerApiMessage +from packages.valory.skills.abstract_round_abci.base import ( + AbstractRound, + LEDGER_API_ADDRESS, +) +from packages.valory.skills.abstract_round_abci.behaviours import ( + AbstractRoundBehaviour, + BaseBehaviour, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + LedgerApiDialogue, + LedgerApiDialogues, +) +from packages.valory.skills.abstract_round_abci.io_.store import SupportedFiletype +from packages.valory.skills.abstract_round_abci.models import ApiSpecs, Requests +from packages.valory.skills.portfolio_tracker_abci.models import ( + GetBalance, + Params, + RPCPayload, + TokenAccounts, +) +from packages.valory.skills.portfolio_tracker_abci.payloads import ( + PortfolioTrackerPayload, +) +from packages.valory.skills.portfolio_tracker_abci.rounds import ( + PortfolioTrackerAbciApp, + PortfolioTrackerRound, + SynchronizedData, +) + + +ledger_to_native_mapping = { + "ethereum": ("ETH", 18), + "xdai": ("XDAI", 18), + "optimism": ("ETH", 18), + "base": ("ETH", 18), +} + +PORTFOLIO_FILENAME = "portfolio.json" +SOL_ADDRESS = "So11111111111111111111111111111111111111112" +BALANCE_METHOD = "getBalance" +TOKEN_ACCOUNTS_METHOD = "getTokenAccountsByOwner" # nosec +TOKEN_ENCODING = "jsonParsed" # nosec +TOKEN_AMOUNT_ACCESS_KEYS = ( + "account", + "data", + "parsed", + "info", + "tokenAmount", + "amount", +) + + +def to_content(content: dict) -> bytes: + """Convert the given content to bytes' payload.""" + return json.dumps(content, sort_keys=True).encode() + + +def safely_get_from_nested_dict( + nested_dict: Dict[str, Any], keys: Tuple[str, ...] +) -> Optional[Any]: + """Get a value safely from a nested dictionary.""" + res = deepcopy(nested_dict) + for key in keys[:-1]: + res = res.get(key, {}) + if not isinstance(res, dict): + return None + + if keys[-1] not in res: + return None + return res[keys[-1]] + + +class PortfolioTrackerBehaviour(BaseBehaviour): + """Behaviour responsible for tracking the portfolio of the service.""" + + matching_round: Type[AbstractRound] = PortfolioTrackerRound + + def __init__(self, **kwargs: Any) -> None: + """Initialize the strategy evaluator behaviour.""" + super().__init__(**kwargs) + self.portfolio: Dict[str, int] = {} + self.portfolio_filepath = Path(self.context.data_dir) / PORTFOLIO_FILENAME + self._performative_to_dialogue_class = { + BalancesMessage.Performative.GET_ALL_BALANCES: self.context.balances_dialogues, + } + + @property + def params(self) -> Params: + """Return the params.""" + return cast(Params, self.context.params) + + @property + def synchronized_data(self) -> SynchronizedData: + """Return the synchronized data.""" + return cast(SynchronizedData, super().synchronized_data) + + @property + def get_balance(self) -> GetBalance: + """Get the `GetBalance` API specs instance.""" + return self.context.get_balance + + @property + def token_accounts(self) -> TokenAccounts: + """Get the `TokenAccounts` API specs instance.""" + return self.context.token_accounts + + @property + def sol_agent_address(self) -> str: + """Get the agent's Solana address.""" + return self.context.agent_addresses[_SOLANA_IDENTIFIER] + + def _handle_response( + self, + api: ApiSpecs, + res: Optional[dict], + ) -> Generator[None, None, Optional[Any]]: + """Handle the response from an API. + + :param api: the `ApiSpecs` instance of the API. + :param res: the response to handle. + :return: the response's result, using the given keys. `None` if response is `None` (has failed). + :yield: None + """ + if res is None: + error = f"Could not get a response from {api.api_id!r} API." + self.context.logger.error(error) + api.increment_retries() + yield from self.sleep(api.retries_info.suggested_sleep_time) + return None + + self.context.logger.info( + f"Retrieved a response from {api.api_id!r} API: {res}." + ) + api.reset_retries() + return res + + def _get_response( + self, + api: ApiSpecs, + dynamic_parameters: Dict[str, str], + content: Optional[dict] = None, + ) -> Generator[None, None, Any]: + """Get the response from an API.""" + specs = api.get_spec() + specs["parameters"].update(dynamic_parameters) + if content is not None: + specs["content"] = to_content(content) + + while not api.is_retries_exceeded(): + res_raw = yield from self.get_http_response(**specs) + res = api.process_response(res_raw) + response = yield from self._handle_response(api, res) + if response is not None: + return response + + error = f"Retries were exceeded for {api.api_id!r} API." + self.context.logger.error(error) + api.reset_retries() + return None + + def get_dcxt_response( + self, + protocol_performative: BalancesMessage.Performative, + **kwargs: Any, + ) -> Generator[None, None, Any]: + """Get a ccxt response.""" + if protocol_performative not in self._performative_to_dialogue_class: + raise ValueError( + f"Unsupported protocol performative {protocol_performative}." + ) + dialogue_class = self._performative_to_dialogue_class[protocol_performative] + + msg, dialogue = dialogue_class.create( + counterparty=str(DCXT_CONNECTION_ID), + performative=protocol_performative, + **kwargs, + ) + msg._sender = str(self.context.skill_id) # pylint: disable=protected-access + response = yield from self._do_request(msg, dialogue) + return response + + def get_solana_native_balance( + self, address: str + ) -> Generator[None, None, Optional[int]]: + """Get the SOL balance of the given address.""" + payload = RPCPayload(BALANCE_METHOD, [address]) + response = yield from self._get_response(self.get_balance, {}, asdict(payload)) + if response is None: + self.context.logger.error("Failed to get SOL balance!") + return response + + def check_solana_balance( + self, multisig: bool + ) -> Generator[None, None, Optional[bool]]: + """Check whether the balance of the multisig or the agent is above the corresponding threshold.""" + if multisig: + address = self.params.squad_vault + theta = self.params.multisig_balance_threshold + which = "vault" + else: + address = self.sol_agent_address + theta = self.params.agent_balance_threshold + which = "agent" + + self.context.logger.info(f"Checking the SOl balance of the {which}...") + balance = yield from self.get_solana_native_balance(address) + if balance is None: + return None + if balance < theta: + self.context.logger.warning( + f"The {which}'s SOL balance is below the specified threshold: {balance} < {theta}" + ) + return False + self.context.logger.info(f"SOL balance of the {which} is sufficient.") + if multisig: + self.portfolio[SOL_ADDRESS] = balance + return True + + def _is_solana_balance_sufficient( + self, ledger_id: str + ) -> Generator[None, None, Optional[bool]]: + """Check whether the balance of the multisig and the agent are above the given thresholds.""" + self.context.logger.info( + f"Checking the SOL balance of the agent and the vault on ledger {ledger_id}..." + ) + agent_balance = yield from self.check_solana_balance(multisig=False) + vault_balance = yield from self.check_solana_balance(multisig=True) + + balances = (agent_balance, vault_balance) + if None in balances: + return None + return all(balances) + + def unexpected_res_format_err(self, res: Any) -> None: + """Error log in case of an unexpected format error.""" + self.context.logger.error( + f"Unexpected response format from {TOKEN_ACCOUNTS_METHOD!r}: {res}" + ) + + def get_token_balance(self, token: str) -> Generator[None, None, Optional[int]]: + """Retrieve the balance of the tokens held in the vault.""" + payload = RPCPayload( + TOKEN_ACCOUNTS_METHOD, + [ + self.params.squad_vault, + {"mint": token}, + {"encoding": TOKEN_ENCODING}, + ], + ) + response = yield from self._get_response( + self.token_accounts, {}, asdict(payload) + ) + if response is None: + return None + + if not isinstance(response, list): + self.unexpected_res_format_err(response) + return None + + if len(response) == 0: + return 0 + + value_content = response.pop(0) + + if not isinstance(value_content, dict): + self.unexpected_res_format_err(response) + return None + + amount = safely_get_from_nested_dict(value_content, TOKEN_AMOUNT_ACCESS_KEYS) + + try: + # typing was warning about int(amount), therefore, we first convert to `str` here + return int(str(amount)) + except ValueError: + self.unexpected_res_format_err(response) + return None + + def _track_solana_portfolio(self, ledger_id: str) -> Generator: + """Track the portfolio of the service.""" + self.context.logger.info( + f"Tracking the portfolio of the service... on ledger {ledger_id}" + ) + should_wait = False + for token in self.params.tracked_tokens: + self.context.logger.info(f"Tracking {token=}...") + + if token == SOL_ADDRESS: + continue + + if should_wait: + yield from self.sleep(self.params.rpc_polling_interval) + should_wait = True + + balance = yield from self.get_solana_token_balance(token) + if balance is None: + self.context.logger.error( + f"Portfolio tracking failed! Could not get the vault's balance for {token=}." + ) + return None + self.portfolio[token] = balance + + def _track_evm_portfolio(self, ledger_id: str) -> Generator: + """Track the portfolio of the service.""" + self.context.logger.info( + f"Tracking the portfolio of the service... on ledger {ledger_id}" + ) + + for exchange in self.params.exchange_ids[ledger_id]: + exchange_id = f"{exchange}" + self.context.logger.info(f"Tracking {exchange_id=} on {ledger_id=}...") + + balances_msg = yield from self.get_dcxt_response( + BalancesMessage.Performative.GET_ALL_BALANCES, # type: ignore + exchange_id=exchange_id, + ledger_id=ledger_id, + address=self.context.params.setup_params["safe_contract_address"], + params={}, + ) + for balance in balances_msg.balances.balances: + self.context.logger.info( + f"Retrieved balance from {exchange_id}: {balance}" + ) + self.portfolio[balance.asset_id] = balance.free + # We also store the balance in the agent's address + # TODO: we implement a mapping of ledger to exchanges, + # so that we can also track the portfolio of the address across different exchanges. + + + def async_act(self) -> Generator: + """Do the act, supporting asynchronous execution.""" + + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + for ledger_id in self.params.ledger_ids: + if ledger_id == _SOLANA_IDENTIFIER: + is_balance_sufficient_func = self._is_solana_balance_sufficient + track_portfolio_func = self._track_solana_portfolio + else: + is_balance_sufficient_func = self._is_evm_balance_sufficient + track_portfolio_func = self._track_evm_portfolio + + if self.synchronized_data.is_balance_sufficient is False: + # wait for some time for the user to take action + sleep_time = self.params.refill_action_timeout + self.context.logger.info( + f"Waiting for a refill. Checking again in {sleep_time} seconds..." + ) + yield from self.sleep(sleep_time) + + is_balance_sufficient = yield from is_balance_sufficient_func(ledger_id) + if is_balance_sufficient is None: + portfolio_hash = None + elif not is_balance_sufficient: + # the value does not matter as the round will transition based on the insufficient balance event + portfolio_hash = "" + else: + yield from track_portfolio_func(ledger_id) + portfolio_hash = yield from self.send_to_ipfs( + str(self.portfolio_filepath), + self.portfolio, + filetype=SupportedFiletype.JSON, + ) + if portfolio_hash is None: + is_balance_sufficient = None + + sender = self.context.agent_address + payload = PortfolioTrackerPayload( + sender, portfolio_hash, is_balance_sufficient + ) + + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + yield from self.send_a2a_transaction(payload) + yield from self.wait_until_round_end() + + self.set_done() + + def _is_evm_balance_sufficient( + self, ledger_id: str + ) -> Generator[None, None, Optional[bool]]: + """Check whether the balance of the multisig and the agent are above the given thresholds.""" + self.context.logger.info( + f"Checking the balance of the agent and the vault on ledger {ledger_id}..." + ) + agent_balance = yield from self.check_evm_balance( + multisig=False, ledger_id=ledger_id + ) + vault_balance = yield from self.check_evm_balance( + multisig=True, ledger_id=ledger_id + ) + + balances = (agent_balance, vault_balance) + if None in balances: + return None + return all(balances) + + def check_evm_balance( + self, multisig: bool, ledger_id: str + ) -> Generator[None, None, Optional[bool]]: + """Check whether the balance of the multisig or the agent is above the corresponding threshold.""" + if multisig: + address = self.params.setup_params["safe_contract_address"] + theta = self.params.multisig_balance_threshold + which = "vault" + else: + address = self.context.agent_address + theta = self.params.agent_balance_threshold + which = "agent" + + self.context.logger.info(f"Checking the balance of the {which}...") + balance = yield from self.get_evm_native_balance(address, ledger_id) + # We store in the portfolio the balance of the agent + if balance is None: + return None + self.portfolio[ledger_to_native_mapping[ledger_id][0]] = balance + if balance < theta: + self.context.logger.warning( + f"The {which}'s balance is below the specified threshold: {balance} < {theta}" + ) + return False + self.context.logger.info(f"Balance of the {which} is sufficient.") + return True + + def get_evm_native_balance( + self, address: str, ledger_id: str + ) -> Generator[None, None, Optional[int]]: + """Get the balance of the given address.""" + # We send a request to the ledger to get the balance of the agent. + ledger_api_msg = yield from self.get_ledger_api_response( + address=address, + performative=LedgerApiMessage.Performative.GET_BALANCE, # type: ignore + ledger_id=ledger_id, + ledger_callable="get_balance", + ) + + if ledger_api_msg.performative == LedgerApiMessage.Performative.ERROR: + self.context.logger.error( + f"Failed to get the balance of the agent from ledger {ledger_id}! with error: {ledger_api_msg}" + ) + return None + elif ledger_api_msg.performative == LedgerApiMessage.Performative.BALANCE: + balance = ledger_api_msg.balance + self.context.logger.info( + f"Retrieved balance from ledger {ledger_id}: {balance}" + ) + return balance + else: + self.context.logger.error( + f"Unexpected performative from ledger {ledger_id}: {ledger_api_msg}" + ) + return None + + def get_ledger_api_response( # type: ignore + self, + performative: LedgerApiMessage.Performative, + ledger_callable: str, + ledger_id: str, + address: str, + **kwargs: Any, + ) -> Generator[None, None, LedgerApiMessage]: + """ + Request data from ledger api + + Happy-path full flow of the messages. + + AbstractRoundAbci skill -> (LedgerApiMessage | LedgerApiMessage.Performative) -> Ledger connection + Ledger connection -> (LedgerApiMessage | LedgerApiMessage.Performative) -> AbstractRoundAbci skill + + :param performative: the message performative + :param ledger_callable: the callable to call on the contract + :param kwargs: keyword argument for the contract api request + :return: the contract api response + :yields: the contract api response + """ + ledger_api_dialogues = cast( + LedgerApiDialogues, self.context.ledger_api_dialogues + ) + kwargs = { + "performative": performative, + "counterparty": LEDGER_API_ADDRESS, + "ledger_id": "ethereum", + "callable": ledger_callable, + "address": address, + "kwargs": Kwargs( + { + "chain_id": ledger_id, + } + ), + } + ledger_api_msg, ledger_api_dialogue = ledger_api_dialogues.create(**kwargs) + ledger_api_dialogue = cast( + LedgerApiDialogue, + ledger_api_dialogue, + ) + ledger_api_dialogue.terms = self._get_default_terms() + request_nonce = self._get_request_nonce_from_dialogue(ledger_api_dialogue) + cast(Requests, self.context.requests).request_id_to_callback[ + request_nonce + ] = self.get_callback_request() + self.context.outbox.put_message(message=ledger_api_msg) + response = yield from self.wait_for_message() + return response + + +class PortfolioTrackerRoundBehaviour(AbstractRoundBehaviour): + """PortfolioTrackerRoundBehaviour""" + + initial_behaviour_cls = PortfolioTrackerBehaviour + abci_app_cls = PortfolioTrackerAbciApp + behaviours: Set[Type[BaseBehaviour]] = { + PortfolioTrackerBehaviour, # type: ignore + } diff --git a/packages/valory/skills/portfolio_tracker_abci/dialogues.py b/packages/valory/skills/portfolio_tracker_abci/dialogues.py new file mode 100644 index 0000000..1739e44 --- /dev/null +++ b/packages/valory/skills/portfolio_tracker_abci/dialogues.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the dialogues of the FSM app.""" + +from packages.eightballer.protocols.balances.dialogues import ( + BalancesDialogue as BaseBalancesDialogue, +) +from packages.eightballer.protocols.balances.dialogues import ( + BalancesDialogues as BaseBalancesDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + AbciDialogue as BaseAbciDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + AbciDialogues as BaseAbciDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + ContractApiDialogue as BaseContractApiDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + ContractApiDialogues as BaseContractApiDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + HttpDialogue as BaseHttpDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + HttpDialogues as BaseHttpDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + IpfsDialogue as BaseIpfsDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + IpfsDialogues as BaseIpfsDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + LedgerApiDialogue as BaseLedgerApiDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + LedgerApiDialogues as BaseLedgerApiDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + SigningDialogue as BaseSigningDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + SigningDialogues as BaseSigningDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + TendermintDialogue as BaseTendermintDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + TendermintDialogues as BaseTendermintDialogues, +) + + +AbciDialogue = BaseAbciDialogue +AbciDialogues = BaseAbciDialogues + + +HttpDialogue = BaseHttpDialogue +HttpDialogues = BaseHttpDialogues + + +SigningDialogue = BaseSigningDialogue +SigningDialogues = BaseSigningDialogues + + +LedgerApiDialogue = BaseLedgerApiDialogue +LedgerApiDialogues = BaseLedgerApiDialogues + + +ContractApiDialogue = BaseContractApiDialogue +ContractApiDialogues = BaseContractApiDialogues + + +TendermintDialogue = BaseTendermintDialogue +TendermintDialogues = BaseTendermintDialogues + + +IpfsDialogue = BaseIpfsDialogue +IpfsDialogues = BaseIpfsDialogues + + +BaseBalancesDialogue = BaseBalancesDialogue +BaseBalancesDialogues = BaseBalancesDialogues diff --git a/packages/valory/skills/portfolio_tracker_abci/fsm_specification.yaml b/packages/valory/skills/portfolio_tracker_abci/fsm_specification.yaml new file mode 100644 index 0000000..947811e --- /dev/null +++ b/packages/valory/skills/portfolio_tracker_abci/fsm_specification.yaml @@ -0,0 +1,23 @@ +alphabet_in: +- DONE +- FAILED +- INSUFFICIENT_BALANCE +- NO_MAJORITY +- ROUND_TIMEOUT +default_start_state: PortfolioTrackerRound +final_states: +- FailedPortfolioTrackerRound +- FinishedPortfolioTrackerRound +label: PortfolioTrackerAbciApp +start_states: +- PortfolioTrackerRound +states: +- FailedPortfolioTrackerRound +- FinishedPortfolioTrackerRound +- PortfolioTrackerRound +transition_func: + (PortfolioTrackerRound, DONE): FinishedPortfolioTrackerRound + (PortfolioTrackerRound, FAILED): FailedPortfolioTrackerRound + (PortfolioTrackerRound, INSUFFICIENT_BALANCE): PortfolioTrackerRound + (PortfolioTrackerRound, NO_MAJORITY): PortfolioTrackerRound + (PortfolioTrackerRound, ROUND_TIMEOUT): PortfolioTrackerRound diff --git a/packages/valory/skills/portfolio_tracker_abci/handlers.py b/packages/valory/skills/portfolio_tracker_abci/handlers.py new file mode 100644 index 0000000..f2c6dbf --- /dev/null +++ b/packages/valory/skills/portfolio_tracker_abci/handlers.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the handlers of the FSM app.""" + +from packages.eightballer.protocols.balances.message import BalancesMessage +from packages.valory.skills.abstract_round_abci.handlers import ( + ABCIRoundHandler as BaseABCIRoundHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import AbstractResponseHandler +from packages.valory.skills.abstract_round_abci.handlers import ( + ContractApiHandler as BaseContractApiHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + HttpHandler as BaseHttpHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + IpfsHandler as BaseIpfsHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + LedgerApiHandler as BaseLedgerApiHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + SigningHandler as BaseSigningHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + TendermintHandler as BaseTendermintHandler, +) + + +class DcxtBalancesHandler(AbstractResponseHandler): + """This class implements a handler for DexBalancesHandler messages.""" + + SUPPORTED_PROTOCOL = BalancesMessage.protocol_id + allowed_response_performatives = frozenset( + { + BalancesMessage.Performative.ALL_BALANCES, + BalancesMessage.Performative.BALANCE, + BalancesMessage.Performative.ERROR, + } + ) + + +ABCIHandler = BaseABCIRoundHandler +HttpHandler = BaseHttpHandler +SigningHandler = BaseSigningHandler +LedgerApiHandler = BaseLedgerApiHandler +ContractApiHandler = BaseContractApiHandler +TendermintHandler = BaseTendermintHandler +IpfsHandler = BaseIpfsHandler diff --git a/packages/valory/skills/portfolio_tracker_abci/models.py b/packages/valory/skills/portfolio_tracker_abci/models.py new file mode 100644 index 0000000..6ccba44 --- /dev/null +++ b/packages/valory/skills/portfolio_tracker_abci/models.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the models for the Portfolio Tracker.""" + +from dataclasses import asdict, dataclass +from typing import Any, Dict, Iterable, List, Union + +from packages.valory.skills.abstract_round_abci.models import ApiSpecs, BaseParams +from packages.valory.skills.abstract_round_abci.models import ( + BenchmarkTool as BaseBenchmarkTool, +) +from packages.valory.skills.abstract_round_abci.models import Requests as BaseRequests +from packages.valory.skills.abstract_round_abci.models import ( + SharedState as BaseSharedState, +) +from packages.valory.skills.portfolio_tracker_abci.rounds import PortfolioTrackerAbciApp + + +Requests = BaseRequests +BenchmarkTool = BaseBenchmarkTool + + +class GetBalance(ApiSpecs): + """A model that wraps ApiSpecs for the Solana balance check.""" + + +class TokenAccounts(ApiSpecs): + """A model that wraps ApiSpecs for the Solana tokens' balance check.""" + + +class SharedState(BaseSharedState): + """Keep the current shared state of the skill.""" + + abci_app_cls = PortfolioTrackerAbciApp + + +class Params(BaseParams): + """Parameters.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize the parameters object.""" + self.agent_balance_threshold: int = self._ensure( + "agent_balance_threshold", kwargs, int + ) + self.multisig_balance_threshold: int = self._ensure( + "multisig_balance_threshold", kwargs, int + ) + self.squad_vault: str = self._ensure("squad_vault", kwargs, str) + self.tracked_tokens: List[str] = self._ensure( + "tracked_tokens", kwargs, List[str] + ) + self.refill_action_timeout: int = self._ensure( + "refill_action_timeout", kwargs, int + ) + self.rpc_polling_interval: int = self._ensure( + "rpc_polling_interval", kwargs, int + ) + # We depend on the same keys across all the models, so we can just use the same keys. + if not getattr(self, "ledger_ids", None): + self.ledger_ids = self._ensure("ledger_ids", kwargs, List[str]) + if not getattr(self, "exchange_ids", None): + self.exchange_ids = self._ensure( + "exchange_ids", kwargs, Dict[str, List[str]] + ) + super().__init__(*args, **kwargs) + + +@dataclass +class RPCPayload: + """An RPC request's payload.""" + + method: str + params: list + id: int = 1 + jsonrpc: str = "2.0" + + def __getitem__(self, attr: str) -> Union[int, str, list]: + """Implemented so we can easily unpack using `**`.""" + return getattr(self, attr) + + def keys(self) -> Iterable[str]: + """Implemented so we can easily unpack using `**`.""" + return asdict(self).keys() diff --git a/packages/valory/skills/portfolio_tracker_abci/payloads.py b/packages/valory/skills/portfolio_tracker_abci/payloads.py new file mode 100644 index 0000000..2e2b37a --- /dev/null +++ b/packages/valory/skills/portfolio_tracker_abci/payloads.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the transaction payloads of the PortfolioTrackerAbciApp.""" + +from dataclasses import dataclass +from typing import Optional + +from packages.valory.skills.abstract_round_abci.base import BaseTxPayload + + +@dataclass(frozen=True) +class PortfolioTrackerPayload(BaseTxPayload): + """Represent a transaction payload for the portfolio tracker.""" + + portfolio_hash: Optional[str] + is_balance_sufficient: Optional[bool] diff --git a/packages/valory/skills/portfolio_tracker_abci/rounds.py b/packages/valory/skills/portfolio_tracker_abci/rounds.py new file mode 100644 index 0000000..c893f2f --- /dev/null +++ b/packages/valory/skills/portfolio_tracker_abci/rounds.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains the rounds of PortfolioTrackerAbciApp.""" + +from enum import Enum +from typing import Dict, Optional, Set, Tuple, cast + +from packages.valory.skills.abstract_round_abci.base import ( + AbciApp, + AbciAppTransitionFunction, + AppState, + BaseSynchronizedData, + CollectSameUntilThresholdRound, + CollectionRound, + DegenerateRound, + DeserializedCollection, + EventToTimeout, + get_name, +) +from packages.valory.skills.portfolio_tracker_abci.payloads import ( + PortfolioTrackerPayload, +) + + +class Event(Enum): + """PortfolioTrackerAbciApp Events""" + + DONE = "done" + INSUFFICIENT_BALANCE = "insufficient_balance" + FAILED = "failed" + NO_MAJORITY = "no_majority" + ROUND_TIMEOUT = "round_timeout" + + +class SynchronizedData(BaseSynchronizedData): + """ + Class to represent the synchronized data. + + This data is replicated by the tendermint application. + """ + + def _get_deserialized(self, key: str) -> DeserializedCollection: + """Strictly get a collection and return it deserialized.""" + serialized = self.db.get_strict(key) + return CollectionRound.deserialize_collection(serialized) + + @property + def portfolio_hash(self) -> Optional[str]: + """Get the hash of the portfolio's data.""" + return self.db.get_strict("portfolio_hash") + + @property + def is_balance_sufficient(self) -> Optional[bool]: + """Get whether the balance is sufficient.""" + return self.db.get("is_balance_sufficient", None) + + @property + def participant_to_portfolio(self) -> DeserializedCollection: + """Get the participants to portfolio tracking.""" + return self._get_deserialized("participant_to_portfolio") + + +class PortfolioTrackerRound(CollectSameUntilThresholdRound): + """PortfolioTrackerRound""" + + payload_class = PortfolioTrackerPayload + synchronized_data_class = SynchronizedData + done_event = Event.DONE + none_event = Event.FAILED + no_majority_event = Event.NO_MAJORITY + selection_key = ( + get_name(SynchronizedData.portfolio_hash), + get_name(SynchronizedData.is_balance_sufficient), + ) + collection_key = get_name(SynchronizedData.participant_to_portfolio) + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: + """Process the end of the block.""" + res = super().end_block() + if res is None: + return None + + synced_data, event = cast(Tuple[SynchronizedData, Enum], res) + if event == self.done_event and not synced_data.is_balance_sufficient: + return synced_data, Event.INSUFFICIENT_BALANCE + return synced_data, event + + +class FinishedPortfolioTrackerRound(DegenerateRound): + """This class represents that the portfolio tracking has finished.""" + + +class FailedPortfolioTrackerRound(DegenerateRound): + """This class represents that the portfolio tracking has failed.""" + + +class PortfolioTrackerAbciApp(AbciApp[Event]): + """PortfolioTrackerAbciApp + + Initial round: PortfolioTrackerRound + + Initial states: {PortfolioTrackerRound} + + Transition states: + 0. PortfolioTrackerRound + - done: 1. + - failed: 2. + - insufficient balance: 0. + - no majority: 0. + - round timeout: 0. + 1. FinishedPortfolioTrackerRound + 2. FailedPortfolioTrackerRound + + Final states: {FailedPortfolioTrackerRound, FinishedPortfolioTrackerRound} + + Timeouts: + round timeout: 30.0 + """ + + initial_round_cls: AppState = PortfolioTrackerRound + initial_states: Set[AppState] = {PortfolioTrackerRound} + transition_function: AbciAppTransitionFunction = { + PortfolioTrackerRound: { + Event.DONE: FinishedPortfolioTrackerRound, + Event.FAILED: FailedPortfolioTrackerRound, + Event.INSUFFICIENT_BALANCE: PortfolioTrackerRound, + Event.NO_MAJORITY: PortfolioTrackerRound, + Event.ROUND_TIMEOUT: PortfolioTrackerRound, + }, + FinishedPortfolioTrackerRound: {}, + FailedPortfolioTrackerRound: {}, + } + final_states: Set[AppState] = { + FinishedPortfolioTrackerRound, + FailedPortfolioTrackerRound, + } + event_to_timeout: EventToTimeout = { + Event.ROUND_TIMEOUT: 30.0, + } + db_pre_conditions: Dict[AppState, Set[str]] = { + PortfolioTrackerRound: set(), + } + db_post_conditions: Dict[AppState, Set[str]] = { + FinishedPortfolioTrackerRound: { + get_name(SynchronizedData.portfolio_hash), + get_name(SynchronizedData.is_balance_sufficient), + get_name(SynchronizedData.participant_to_portfolio), + }, + FailedPortfolioTrackerRound: set(), + } diff --git a/packages/valory/skills/portfolio_tracker_abci/skill.yaml b/packages/valory/skills/portfolio_tracker_abci/skill.yaml new file mode 100644 index 0000000..ae3dbc0 --- /dev/null +++ b/packages/valory/skills/portfolio_tracker_abci/skill.yaml @@ -0,0 +1,158 @@ +name: portfolio_tracker_abci +author: valory +version: 0.1.0 +type: skill +description: An ABCI skill responsible for tracking the portfolio of the service +license: Apache-2.0 +aea_version: '>=1.0.0, <2.0.0' +fingerprint: + README.md: bafybeif4z5ogzdq474b2k67kwyit5yiwiyoc4n42ucv3j52v2vcpophtnu + __init__.py: bafybeifc2ovpf3gofxafilnv2jgtbn73wrcpwoss6ic7ftvnk2rwboenza + behaviours.py: bafybeiasowd66cebllflpxvahzrc5o65cjaiwvnpnfhlcz7ylvlbdoyltm + dialogues.py: bafybeibhhuam4c4cjhzv2l56pqkqhinpkiiivhr5akbqg765khwha3xauy + fsm_specification.yaml: bafybeicdgdc4qoaco2et6kcommb753xlw3d4wma3x6t57jhc6hdjzfczoy + handlers.py: bafybeidrynxgp6qyxlk2ert6dawcsji4b7dylnszpnb5gufcdc6tyej2ym + models.py: bafybeian54yk62z474g3ujd7k4xwh7duow5i3so2jnn4jhw3f2d7zellaa + payloads.py: bafybeid3sue7scr5dama3krank3gkmsucy6xasvds6jvebkptbpqziurlm + rounds.py: bafybeihcblnqrf3jwtvmwgfrrcss7bs5ihq4e4kpyld2dnc6uibgwkjs2u +fingerprint_ignore_patterns: [] +connections: +- eightballer/dcxt:0.1.0:bafybeihes2id6qmqqybvdqfpchu6h3i5mpnhq6lztfxp52twd22iwqxacq +contracts: [] +protocols: +- eightballer/balances:0.1.0:bafybeiajh5vzhcofdpemm3545t3yh6g4okpwnejvbqchxapo765batiitu +- valory/ledger_api:1.0.0:bafybeihdk6psr4guxmbcrc26jr2cbgzpd5aljkqvpwo64bvaz7tdti2oni +skills: +- valory/abstract_round_abci:0.1.0:bafybeibovsktd3uxur45nrcomq5shcn46cgxd5idmhxbmjhg32c5abyqim +behaviours: + main: + args: {} + class_name: PortfolioTrackerRoundBehaviour +handlers: + abci: + args: {} + class_name: ABCIHandler + contract_api: + args: {} + class_name: ContractApiHandler + http: + args: {} + class_name: HttpHandler + ipfs: + args: {} + class_name: IpfsHandler + ledger_api: + args: {} + class_name: LedgerApiHandler + signing: + args: {} + class_name: SigningHandler + tendermint: + args: {} + class_name: TendermintHandler + balances: + args: {} + class_name: DcxtBalancesHandler +models: + abci_dialogues: + args: {} + class_name: AbciDialogues + benchmark_tool: + args: + log_dir: /logs + class_name: BenchmarkTool + contract_api_dialogues: + args: {} + class_name: ContractApiDialogues + http_dialogues: + args: {} + class_name: HttpDialogues + ipfs_dialogues: + args: {} + class_name: IpfsDialogues + ledger_api_dialogues: + args: {} + class_name: LedgerApiDialogues + params: + args: + cleanup_history_depth: 1 + cleanup_history_depth_current: null + drand_public_key: 868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31 + finalize_timeout: 60.0 + genesis_config: + chain_id: chain-c4daS1 + consensus_params: + block: + max_bytes: '22020096' + max_gas: '-1' + time_iota_ms: '1000' + evidence: + max_age_duration: '172800000000000' + max_age_num_blocks: '100000' + max_bytes: '1048576' + validator: + pub_key_types: + - ed25519 + version: {} + genesis_time: '2022-05-20T16:00:21.735122717Z' + voting_power: '10' + history_check_timeout: 1205 + ipfs_domain_name: null + keeper_allowed_retries: 3 + keeper_timeout: 30.0 + max_attempts: 10 + max_healthcheck: 120 + on_chain_service_id: null + request_retry_delay: 1.0 + request_timeout: 10.0 + reset_pause_duration: 10 + reset_tendermint_after: 2 + retry_attempts: 400 + retry_timeout: 3 + round_timeout_seconds: 30.0 + service_id: portfolio_tracker + service_registry_address: null + setup: + all_participants: + - '0x0000000000000000000000000000000000000000' + consensus_threshold: null + safe_contract_address: '0x0000000000000000000000000000000000000000' + share_tm_config_on_startup: false + sleep_time: 1 + tendermint_check_sleep_delay: 3 + tendermint_com_url: http://localhost:8080 + tendermint_max_retries: 5 + tendermint_p2p_url: localhost:26656 + tendermint_url: http://localhost:26657 + tx_timeout: 10.0 + validate_timeout: 1205 + squad_vault: 39Zh4C687EXLY7CT8gjCxe2hUc3krESjUsqs7A1CKD5E + agent_balance_threshold: 50000000 + multisig_balance_threshold: 1000000000 + tracked_tokens: [] + refill_action_timeout: 10 + rpc_polling_interval: 5 + ledger_ids: + - optimism + exchange_ids: + ethereum: [] + optimism: + - balancer + class_name: Params + requests: + args: {} + class_name: Requests + signing_dialogues: + args: {} + class_name: SigningDialogues + state: + args: {} + class_name: SharedState + tendermint_dialogues: + args: {} + class_name: TendermintDialogues + balances_dialogues: + args: {} + class_name: BalancesDialogues +dependencies: {} +is_abstract: true diff --git a/packages/valory/skills/registration_abci/README.md b/packages/valory/skills/registration_abci/README.md new file mode 100644 index 0000000..a8eeb22 --- /dev/null +++ b/packages/valory/skills/registration_abci/README.md @@ -0,0 +1,25 @@ +# Registration abci + +## Description + +This module contains the ABCI registration skill for an AEA. + +## Behaviours + +* `RegistrationBaseBehaviour` + + Register to the next periods. + +* `RegistrationBehaviour` + + Register to the next periods. + +* `RegistrationStartupBehaviour` + + Register to the next periods. + + +## Handlers + +No Handlers (the skill is purely behavioural). + diff --git a/packages/valory/skills/registration_abci/__init__.py b/packages/valory/skills/registration_abci/__init__.py new file mode 100644 index 0000000..e69b708 --- /dev/null +++ b/packages/valory/skills/registration_abci/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the ABCI registration skill for an AEA.""" + +from aea.configurations.base import PublicId + + +PUBLIC_ID = PublicId.from_str("valory/registration_abci:0.1.0") diff --git a/packages/valory/skills/registration_abci/behaviours.py b/packages/valory/skills/registration_abci/behaviours.py new file mode 100644 index 0000000..4a617b4 --- /dev/null +++ b/packages/valory/skills/registration_abci/behaviours.py @@ -0,0 +1,492 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the behaviours for the 'registration_abci' skill.""" + +import datetime +import json +from abc import ABC +from enum import Enum +from typing import Any, Dict, Generator, Optional, Set, Type, cast + +from aea.mail.base import EnvelopeContext + +from packages.valory.connections.p2p_libp2p_client.connection import ( + PUBLIC_ID as P2P_LIBP2P_CLIENT_PUBLIC_ID, +) +from packages.valory.contracts.service_registry.contract import ServiceRegistryContract +from packages.valory.protocols.contract_api import ContractApiMessage +from packages.valory.protocols.http import HttpMessage +from packages.valory.protocols.tendermint import TendermintMessage +from packages.valory.skills.abstract_round_abci.base import ABCIAppInternalError +from packages.valory.skills.abstract_round_abci.behaviour_utils import TimeoutException +from packages.valory.skills.abstract_round_abci.behaviours import ( + AbstractRoundBehaviour, + BaseBehaviour, +) +from packages.valory.skills.abstract_round_abci.utils import parse_tendermint_p2p_url +from packages.valory.skills.registration_abci.dialogues import TendermintDialogues +from packages.valory.skills.registration_abci.models import SharedState +from packages.valory.skills.registration_abci.payloads import RegistrationPayload +from packages.valory.skills.registration_abci.rounds import ( + AgentRegistrationAbciApp, + RegistrationRound, + RegistrationStartupRound, +) + + +NODE = "node_{address}" +WAIT_FOR_BLOCK_TIMEOUT = 60.0 # 1 minute + + +class RegistrationBaseBehaviour(BaseBehaviour, ABC): + """Agent registration to the FSM App.""" + + def async_act(self) -> Generator: + """ + Do the action. + + Steps: + - Build a registration transaction. + - Send the transaction and wait for it to be mined. + - Wait until ABCI application transitions to the next round. + - Go to the next behaviour (set done event). + """ + + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + serialized_db = self.synchronized_data.db.serialize() + payload = RegistrationPayload( + self.context.agent_address, initialisation=serialized_db + ) + + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + yield from self.send_a2a_transaction(payload) + yield from self.wait_until_round_end() + + self.set_done() + + +class RegistrationStartupBehaviour(RegistrationBaseBehaviour): + """Agent registration to the FSM App.""" + + ENCODING: str = "utf-8" + matching_round = RegistrationStartupRound + local_tendermint_params: Dict[str, Any] = {} + updated_genesis_data: Dict[str, Any] = {} + collection_complete: bool = False + + @property + def initial_tm_configs(self) -> Dict[str, Dict[str, Any]]: + """A mapping of the other agents' addresses to their initial Tendermint configuration.""" + return self.context.state.initial_tm_configs + + @initial_tm_configs.setter + def initial_tm_configs(self, configs: Dict[str, Dict[str, Any]]) -> None: + """A mapping of the other agents' addresses to their initial Tendermint configuration.""" + self.context.state.initial_tm_configs = configs + + class LogMessages(Enum): + """Log messages used in RegistrationStartupBehaviour""" + + config_sharing = "Sharing Tendermint config on start-up?" + # request personal tendermint configuration + request_personal = "Request validator config from personal Tendermint node" + response_personal = "Response validator config from personal Tendermint node" + failed_personal = "Failed validator config from personal Tendermint node" + # verify deployment on-chain contract + request_verification = "Request service registry contract verification" + response_verification = "Response service registry contract verification" + failed_verification = "Failed service registry contract verification" + # request service info from on-chain contract + request_service_info = "Request on-chain service info" + response_service_info = "Response on-chain service info" + failed_service_info = "Failed on-chain service info" + # request tendermint configuration other agents + request_others = "Request Tendermint config info from other agents" + collection_complete = "Completed collecting Tendermint configuration responses" + # update personal tendermint node config + request_update = "Request update Tendermint node configuration" + response_update = "Response update Tendermint node configuration" + failed_update = "Failed update Tendermint node configuration" + # exceptions + no_contract_address = "Service registry contract address not provided" + no_on_chain_service_id = "On-chain service id not provided" + contract_incorrect = "Service registry contract not correctly deployed" + no_agents_registered = "No agents registered on-chain" + self_not_registered = "This agent is not registered on-chain" + + def __str__(self) -> str: + """For ease of use in formatted string literals""" + return self.value + + @property + def tendermint_parameter_url(self) -> str: + """Tendermint URL for obtaining and updating parameters""" + return f"{self.params.tendermint_com_url}/params" + + def _decode_result( + self, message: HttpMessage, error_log: LogMessages + ) -> Optional[Dict[str, Any]]: + """Decode a http message's body. + + :param message: the http message. + :param error_log: a log to prefix potential errors with. + :return: the message's body, as a dictionary + """ + try: + response = json.loads(message.body.decode()) + except json.JSONDecodeError as error: + self.context.logger.error(f"{error_log}: {error}") + return None + + if not response["status"]: # pragma: no cover + self.context.logger.error(f"{error_log}: {response['error']}") + return None + + return response + + def is_correct_contract( + self, service_registry_address: str + ) -> Generator[None, None, bool]: + """Contract deployment verification.""" + + self.context.logger.info(self.LogMessages.request_verification) + + performative = ContractApiMessage.Performative.GET_STATE + kwargs = dict( + performative=performative, + contract_address=service_registry_address, + contract_id=str(ServiceRegistryContract.contract_id), + contract_callable="verify_contract", + chain_id=self.params.default_chain_id, + ) + contract_api_response = yield from self.get_contract_api_response(**kwargs) # type: ignore + if ( + contract_api_response.performative + is not ContractApiMessage.Performative.STATE + ): + verified = False + log_method = self.context.logger.error + log_message = f"{self.LogMessages.failed_verification} ({kwargs}): {contract_api_response}" + else: + verified = cast(bool, contract_api_response.state.body["verified"]) + log_method = self.context.logger.info + log_message = f"{self.LogMessages.response_verification}: {verified}" + + log_method(log_message) + return verified + + def get_agent_instances( + self, service_registry_address: str, on_chain_service_id: int + ) -> Generator[None, None, Dict[str, Any]]: + """Get service info available on-chain""" + + log_message = self.LogMessages.request_service_info + self.context.logger.info(f"{log_message}") + + performative = ContractApiMessage.Performative.GET_STATE + kwargs = dict( + performative=performative, + contract_address=service_registry_address, + contract_id=str(ServiceRegistryContract.contract_id), + contract_callable="get_agent_instances", + service_id=on_chain_service_id, + chain_id=self.params.default_chain_id, + ) + contract_api_response = yield from self.get_contract_api_response(**kwargs) # type: ignore + if contract_api_response.performative != ContractApiMessage.Performative.STATE: + log_message = self.LogMessages.failed_service_info + self.context.logger.error( + f"{log_message} ({kwargs}): {contract_api_response}" + ) + return {} + + log_message = self.LogMessages.response_service_info + self.context.logger.info(f"{log_message}: {contract_api_response}") + return cast(dict, contract_api_response.state.body) + + def get_addresses(self) -> Generator: # pylint: disable=too-many-return-statements + """Get addresses of agents registered for the service""" + + service_registry_address = self.params.service_registry_address + if service_registry_address is None: + log_message = self.LogMessages.no_contract_address.value + self.context.logger.error(log_message) + return False + + correctly_deployed = yield from self.is_correct_contract( + service_registry_address + ) + if not correctly_deployed: + return False + + on_chain_service_id = self.params.on_chain_service_id + if on_chain_service_id is None: + log_message = self.LogMessages.no_on_chain_service_id.value + self.context.logger.error(log_message) + return False + + service_info = yield from self.get_agent_instances( + service_registry_address, on_chain_service_id + ) + if not service_info: + return False + + registered_addresses = set(service_info["agentInstances"]) + if not registered_addresses: + log_message = self.LogMessages.no_agents_registered.value + self.context.logger.error(f"{log_message}: {service_info}") + return False + + my_address = self.context.agent_address + if my_address not in registered_addresses: + log_message = f"{self.LogMessages.self_not_registered} ({my_address})" + self.context.logger.error(f"{log_message}: {registered_addresses}") + return False + + # put service info in the shared state for p2p message handler + info: Dict[str, Dict[str, str]] = {i: {} for i in registered_addresses} + tm_host, tm_port = parse_tendermint_p2p_url(url=self.params.tendermint_p2p_url) + validator_config = dict( + hostname=tm_host, + p2p_port=tm_port, + address=self.local_tendermint_params["address"], + pub_key=self.local_tendermint_params["pub_key"], + peer_id=self.local_tendermint_params["peer_id"], + ) + info[self.context.agent_address] = validator_config + self.initial_tm_configs = info + log_message = self.LogMessages.response_service_info.value + self.context.logger.info(f"{log_message}: {info}") + return True + + def get_tendermint_configuration(self) -> Generator[None, None, bool]: + """Make HTTP GET request to obtain agent's local Tendermint node parameters""" + + url = self.tendermint_parameter_url + log_message = self.LogMessages.request_personal + self.context.logger.info(f"{log_message}: {url}") + + result = yield from self.get_http_response(method="GET", url=url) + response = self._decode_result(result, self.LogMessages.failed_personal) + if response is None: + return False + + self.local_tendermint_params = response["params"] + log_message = self.LogMessages.response_personal + self.context.logger.info(f"{log_message}: {response}") + return True + + def request_tendermint_info(self) -> Generator[None, None, bool]: + """Request Tendermint info from other agents""" + + still_missing = {k for k, v in self.initial_tm_configs.items() if not v} - { + self.context.agent_address + } + log_message = self.LogMessages.request_others + self.context.logger.info(f"{log_message}: {still_missing}") + + for address in still_missing: + dialogues = cast(TendermintDialogues, self.context.tendermint_dialogues) + performative = TendermintMessage.Performative.GET_GENESIS_INFO + message, _ = dialogues.create( + counterparty=address, performative=performative + ) + message = cast(TendermintMessage, message) + context = EnvelopeContext(connection_id=P2P_LIBP2P_CLIENT_PUBLIC_ID) + self.context.outbox.put_message(message=message, context=context) + # we wait for the messages that were put in the outbox. + yield from self.sleep(self.params.sleep_time) + + if all(self.initial_tm_configs.values()): + log_message = self.LogMessages.collection_complete + self.context.logger.info(f"{log_message}: {self.initial_tm_configs}") + validator_to_agent = { + config["address"]: agent + for agent, config in self.initial_tm_configs.items() + } + self.context.state.setup_slashing(validator_to_agent) + self.collection_complete = True + return self.collection_complete + + def format_genesis_data( + self, + collected_agent_info: Dict[str, Any], + ) -> Dict[str, Any]: + """Format collected agent info for genesis update""" + + validators = [] + for address, validator_config in collected_agent_info.items(): + validator = dict( + hostname=validator_config["hostname"], + p2p_port=validator_config["p2p_port"], + address=validator_config["address"], + pub_key=validator_config["pub_key"], + peer_id=validator_config["peer_id"], + power=self.params.genesis_config.voting_power, + name=NODE.format(address=address[2:]), # skip 0x part + ) + validators.append(validator) + + genesis_data = dict( + validators=validators, + genesis_config=self.params.genesis_config.to_json(), + external_address=self.params.tendermint_p2p_url, + ) + return genesis_data + + def request_update(self) -> Generator[None, None, bool]: + """Make HTTP POST request to update agent's local Tendermint node""" + + url = self.tendermint_parameter_url + genesis_data = self.format_genesis_data(self.initial_tm_configs) + log_message = self.LogMessages.request_update + self.context.logger.info(f"{log_message}: {genesis_data}") + + content = json.dumps(genesis_data).encode(self.ENCODING) + result = yield from self.get_http_response( + method="POST", url=url, content=content + ) + response = self._decode_result(result, self.LogMessages.failed_update) + if response is None: + return False + + log_message = self.LogMessages.response_update + self.context.logger.info(f"{log_message}: {response}") + self.updated_genesis_data.update(genesis_data) + return True + + def wait_for_block(self, timeout: float) -> Generator[None, None, bool]: + """Wait for a block to be received in the specified timeout.""" + # every agent will finish with the reset at a different time + # hence the following will be different for all agents + start_time = datetime.datetime.now() + + def received_block() -> bool: + """Check whether we have received a block after "start_time".""" + try: + shared_state = cast(SharedState, self.context.state) + last_timestamp = shared_state.round_sequence.last_timestamp + if last_timestamp > start_time: + return True + return False + except ABCIAppInternalError: + # this can happen if we haven't received a block yet + return False + + try: + yield from self.wait_for_condition( + condition=received_block, timeout=timeout + ) + # if the `wait_for_condition` finish without an exception, + # it means that the condition has been satisfied on time + return True + except TimeoutException: + # the agent wasn't able to receive blocks in the given amount of time (timeout) + return False + + def async_act(self) -> Generator: # pylint: disable=too-many-return-statements + """ + Do the action. + + Steps: + 1. Collect personal Tendermint configuration + 2. Make Service Registry contract call to retrieve addresses + of the other agents registered on-chain for the service. + 3. Request Tendermint configuration from registered agents. + This is done over the Agent Communication Network using + the p2p_libp2p_client connection. + 4. Update Tendermint configuration via genesis.json with the + information of the other validators (agents). + 5. Restart Tendermint to establish the validator network. + """ + + exchange_config = self.params.share_tm_config_on_startup + log_message = self.LogMessages.config_sharing.value + self.context.logger.info(f"{log_message}: {exchange_config}") + + if not exchange_config: + yield from super().async_act() + return + + self.context.logger.info(f"My address: {self.context.agent_address}") + + # collect personal Tendermint configuration + if not self.local_tendermint_params: + successful = yield from self.get_tendermint_configuration() + if not successful: + yield from self.sleep(self.params.sleep_time) + return + + # if the agent doesn't have it's tm config info set, then make service registry contract call + # to get the rest of the agents, so we can get their tm config info later + info = self.initial_tm_configs.get(self.context.agent_address, None) + if info is None: + successful = yield from self.get_addresses() + if not successful: + yield from self.sleep(self.params.sleep_time) + return + + # collect Tendermint config information from other agents + if not self.collection_complete: + successful = yield from self.request_tendermint_info() + if not successful: + yield from self.sleep(self.params.sleep_time) + return + + # update Tendermint configuration + if not self.updated_genesis_data: + successful = yield from self.request_update() + if not successful: + yield from self.sleep(self.params.sleep_time) + return + + # restart Tendermint with updated configuration + successful = yield from self.reset_tendermint_with_wait(on_startup=True) + if not successful: + yield from self.sleep(self.params.sleep_time) + return + + # the reset has gone through, and at this point tendermint should start + # sending blocks to the agent. However, that might take a while, since + # we rely on 2/3 of the voting power to be active in order for block production + # to begin. In other words, we wait for >=2/3 of the agents to become active. + successful = yield from self.wait_for_block(timeout=WAIT_FOR_BLOCK_TIMEOUT) + if not successful: + yield from self.sleep(self.params.sleep_time) + return + + yield from super().async_act() + + +class RegistrationBehaviour(RegistrationBaseBehaviour): + """Agent registration to the FSM App.""" + + matching_round = RegistrationRound + + +class AgentRegistrationRoundBehaviour(AbstractRoundBehaviour): + """This behaviour manages the consensus stages for the registration.""" + + initial_behaviour_cls = RegistrationStartupBehaviour + abci_app_cls = AgentRegistrationAbciApp + behaviours: Set[Type[BaseBehaviour]] = { + RegistrationBehaviour, # type: ignore + RegistrationStartupBehaviour, # type: ignore + } diff --git a/packages/valory/skills/registration_abci/dialogues.py b/packages/valory/skills/registration_abci/dialogues.py new file mode 100644 index 0000000..fcc14ac --- /dev/null +++ b/packages/valory/skills/registration_abci/dialogues.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the classes required for dialogue management.""" +from packages.valory.skills.abstract_round_abci.dialogues import ( + AbciDialogue as BaseAbciDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + AbciDialogues as BaseAbciDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + ContractApiDialogue as BaseContractApiDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + ContractApiDialogues as BaseContractApiDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + HttpDialogue as BaseHttpDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + HttpDialogues as BaseHttpDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + IpfsDialogue as BaseIpfsDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + IpfsDialogues as BaseIpfsDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + LedgerApiDialogue as BaseLedgerApiDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + LedgerApiDialogues as BaseLedgerApiDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + SigningDialogue as BaseSigningDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + SigningDialogues as BaseSigningDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + TendermintDialogue as BaseTendermintDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + TendermintDialogues as BaseTendermintDialogues, +) + + +AbciDialogue = BaseAbciDialogue +AbciDialogues = BaseAbciDialogues + + +HttpDialogue = BaseHttpDialogue +HttpDialogues = BaseHttpDialogues + + +SigningDialogue = BaseSigningDialogue +SigningDialogues = BaseSigningDialogues + + +LedgerApiDialogue = BaseLedgerApiDialogue +LedgerApiDialogues = BaseLedgerApiDialogues + + +ContractApiDialogue = BaseContractApiDialogue +ContractApiDialogues = BaseContractApiDialogues + + +TendermintDialogue = BaseTendermintDialogue +TendermintDialogues = BaseTendermintDialogues + + +IpfsDialogue = BaseIpfsDialogue +IpfsDialogues = BaseIpfsDialogues diff --git a/packages/valory/skills/registration_abci/fsm_specification.yaml b/packages/valory/skills/registration_abci/fsm_specification.yaml new file mode 100644 index 0000000..478f352 --- /dev/null +++ b/packages/valory/skills/registration_abci/fsm_specification.yaml @@ -0,0 +1,18 @@ +alphabet_in: +- DONE +- NO_MAJORITY +default_start_state: RegistrationStartupRound +final_states: +- FinishedRegistrationRound +label: AgentRegistrationAbciApp +start_states: +- RegistrationRound +- RegistrationStartupRound +states: +- FinishedRegistrationRound +- RegistrationRound +- RegistrationStartupRound +transition_func: + (RegistrationRound, DONE): FinishedRegistrationRound + (RegistrationRound, NO_MAJORITY): RegistrationRound + (RegistrationStartupRound, DONE): FinishedRegistrationRound diff --git a/packages/valory/skills/registration_abci/handlers.py b/packages/valory/skills/registration_abci/handlers.py new file mode 100644 index 0000000..3605348 --- /dev/null +++ b/packages/valory/skills/registration_abci/handlers.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the handler for the 'registration_abci' skill.""" + +from packages.valory.skills.abstract_round_abci.handlers import ( + ABCIRoundHandler as BaseABCIRoundHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + ContractApiHandler as BaseContractApiHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + HttpHandler as BaseHttpHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + IpfsHandler as BaseIpfsHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + LedgerApiHandler as BaseLedgerApiHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + SigningHandler as BaseSigningHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + TendermintHandler as BaseTendermintHandler, +) + + +ABCIHandler = BaseABCIRoundHandler +HttpHandler = BaseHttpHandler +SigningHandler = BaseSigningHandler +LedgerApiHandler = BaseLedgerApiHandler +ContractApiHandler = BaseContractApiHandler +TendermintHandler = BaseTendermintHandler +IpfsHandler = BaseIpfsHandler diff --git a/packages/valory/skills/registration_abci/models.py b/packages/valory/skills/registration_abci/models.py new file mode 100644 index 0000000..98c7e53 --- /dev/null +++ b/packages/valory/skills/registration_abci/models.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the shared state for the registration abci skill.""" + +from packages.valory.skills.abstract_round_abci.models import BaseParams +from packages.valory.skills.abstract_round_abci.models import ( + BenchmarkTool as BaseBenchmarkTool, +) +from packages.valory.skills.abstract_round_abci.models import Requests as BaseRequests +from packages.valory.skills.abstract_round_abci.models import ( + SharedState as BaseSharedState, +) +from packages.valory.skills.registration_abci.rounds import AgentRegistrationAbciApp + + +class SharedState(BaseSharedState): + """Keep the current shared state of the skill.""" + + abci_app_cls = AgentRegistrationAbciApp + + +Params = BaseParams +Requests = BaseRequests +BenchmarkTool = BaseBenchmarkTool diff --git a/packages/valory/skills/registration_abci/payloads.py b/packages/valory/skills/registration_abci/payloads.py new file mode 100644 index 0000000..bea2fc5 --- /dev/null +++ b/packages/valory/skills/registration_abci/payloads.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the transaction payloads for common apps.""" + +from dataclasses import dataclass + +from packages.valory.skills.abstract_round_abci.base import BaseTxPayload + + +@dataclass(frozen=True) +class RegistrationPayload(BaseTxPayload): + """Represent a transaction payload of type 'registration'.""" + + initialisation: str diff --git a/packages/valory/skills/registration_abci/rounds.py b/packages/valory/skills/registration_abci/rounds.py new file mode 100644 index 0000000..a191ddd --- /dev/null +++ b/packages/valory/skills/registration_abci/rounds.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the data classes for common apps ABCI application.""" + +from enum import Enum +from typing import Dict, Optional, Set, Tuple + +from packages.valory.skills.abstract_round_abci.base import ( + AbciApp, + AbciAppTransitionFunction, + AppState, + BaseSynchronizedData, + CollectSameUntilAllRound, + CollectSameUntilThresholdRound, + DegenerateRound, + SlashingNotConfiguredError, + get_name, +) +from packages.valory.skills.abstract_round_abci.models import BaseParams +from packages.valory.skills.registration_abci.payloads import RegistrationPayload + + +class Event(Enum): + """Event enumeration for the price estimation demo.""" + + DONE = "done" + ROUND_TIMEOUT = "round_timeout" + NO_MAJORITY = "no_majority" + + +class FinishedRegistrationRound(DegenerateRound): + """A round representing that agent registration has finished""" + + +class RegistrationStartupRound(CollectSameUntilAllRound): + """ + A round in which the agents get registered. + + This round waits until all agents have registered. + """ + + payload_class = RegistrationPayload + synchronized_data_class = BaseSynchronizedData + + @property + def params(self) -> BaseParams: + """Return the params.""" + return self.context.params + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: + """Process the end of the block.""" + if not self.collection_threshold_reached: + return None + + try: + _ = self.context.state.round_sequence.offence_status + # only use slashing if it is configured and the `use_slashing` is set to True + if self.params.use_slashing: + self.context.state.round_sequence.enable_slashing() + except SlashingNotConfiguredError: + self.context.logger.warning("Slashing has not been enabled!") + + self.context.state.round_sequence.sync_db_and_slashing(self.common_payload) + + synchronized_data = self.synchronized_data.update( + participants=tuple(sorted(self.collection)), + synchronized_data_class=self.synchronized_data_class, + ) + + return synchronized_data, Event.DONE + + +class RegistrationRound(CollectSameUntilThresholdRound): + """ + A round in which the agents get registered. + + This rounds waits until the threshold of agents has been reached + and then a further x block confirmations. + """ + + payload_class = RegistrationPayload + required_block_confirmations = 10 + done_event = Event.DONE + synchronized_data_class = BaseSynchronizedData + + # this allows rejoining agents to send payloads + _allow_rejoin_payloads = True + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: + """Process the end of the block.""" + if self.threshold_reached: + self.block_confirmations += 1 + if ( + self.threshold_reached + and self.block_confirmations + > self.required_block_confirmations # we also wait here as it gives more (available) agents time to join + ): + self.synchronized_data.db.sync(self.most_voted_payload) + synchronized_data = self.synchronized_data.update( + participants=tuple(sorted(self.collection)), + synchronized_data_class=self.synchronized_data_class, + ) + return synchronized_data, Event.DONE + if ( + not self.is_majority_possible( + self.collection, self.synchronized_data.nb_participants + ) + and self.block_confirmations > self.required_block_confirmations + ): + return self.synchronized_data, Event.NO_MAJORITY + return None + + +class AgentRegistrationAbciApp(AbciApp[Event]): + """AgentRegistrationAbciApp + + Initial round: RegistrationStartupRound + + Initial states: {RegistrationRound, RegistrationStartupRound} + + Transition states: + 0. RegistrationStartupRound + - done: 2. + 1. RegistrationRound + - done: 2. + - no majority: 1. + 2. FinishedRegistrationRound + + Final states: {FinishedRegistrationRound} + + Timeouts: + round timeout: 30.0 + """ + + initial_round_cls: AppState = RegistrationStartupRound + initial_states: Set[AppState] = {RegistrationStartupRound, RegistrationRound} + transition_function: AbciAppTransitionFunction = { + RegistrationStartupRound: { + Event.DONE: FinishedRegistrationRound, + }, + RegistrationRound: { + Event.DONE: FinishedRegistrationRound, + Event.NO_MAJORITY: RegistrationRound, + }, + FinishedRegistrationRound: {}, + } + final_states: Set[AppState] = { + FinishedRegistrationRound, + } + event_to_timeout: Dict[Event, float] = { + Event.ROUND_TIMEOUT: 30.0, + } + db_pre_conditions: Dict[AppState, Set[str]] = { + RegistrationStartupRound: set(), + RegistrationRound: set(), + } + db_post_conditions: Dict[AppState, Set[str]] = { + FinishedRegistrationRound: { + get_name(BaseSynchronizedData.participants), + }, + } diff --git a/packages/valory/skills/registration_abci/skill.yaml b/packages/valory/skills/registration_abci/skill.yaml new file mode 100644 index 0000000..02c579a --- /dev/null +++ b/packages/valory/skills/registration_abci/skill.yaml @@ -0,0 +1,151 @@ +name: registration_abci +author: valory +version: 0.1.0 +type: skill +description: ABCI application for common apps. +license: Apache-2.0 +aea_version: '>=1.0.0, <2.0.0' +fingerprint: + README.md: bafybeieztbubb6yn5umyt5ulknvb2xxppz5ecxaosxqsaejnrcrrwfu2ji + __init__.py: bafybeigqj2uodavhrygpqn6iah3ljp53z54c5fxyh5ykgkxuhh5lof6pda + behaviours.py: bafybeihal7ku3mvwfbcdm3twzuktnet2io3inshsrpnoee5d5o6mbavg5q + dialogues.py: bafybeicm4bqedlyytfo4icqqbyolo36j2hk7pqh32d3zc5yqg75bt4demm + fsm_specification.yaml: bafybeicx5eutgr4lin7mhwr73xhanuzwdmps7pfoy5f2k7gfxmuec4qbyu + handlers.py: bafybeifby6yecei2d7jvxbqrc3tpyemb7xdb4ood2kny5dqja26qnxrf24 + models.py: bafybeifkfjsfkjy2x32cbuoewxujfgpcl3wk3fji6kq27ofr2zcfe7l5oe + payloads.py: bafybeiacrixfazch2a5ydj7jfk2pnvlxwkygqlwzkfmdeldrj4fqgwyyzm + rounds.py: bafybeifch5qouoop77ef6ghsdflzuy7bcgn4upxjuusxalqzbk53vrxj4q + tests/__init__.py: bafybeiab2s4vkmbz5bc4wggcclapdbp65bosv4en5zaazk5dwmldojpqja + tests/test_behaviours.py: bafybeicwlo3y44sf7gzkyzfuzhwqkax4hln3oforbcvy4uitlgleft3cge + tests/test_dialogues.py: bafybeibeqnpzuzgcfb6yz76htslwsbbpenihswbp7j3qdyq42yswjq25l4 + tests/test_handlers.py: bafybeifpnwaktxckbvclklo6flkm5zqs7apmb33ffs4jrmunoykjbl5lni + tests/test_models.py: bafybeiewxl7nio5av2aukql2u7hlhodzdvjjneleba32abr42xeirrycb4 + tests/test_payloads.py: bafybeifik6ek75ughyd4y6t2lchlmjadkzbrz4hsb332k6ul4pwhlo2oga + tests/test_rounds.py: bafybeidk4d3w5csj6ka7mcq3ikjmv2yccbpwxhp27ujvd7huag3zl5vu2m +fingerprint_ignore_patterns: [] +connections: +- valory/p2p_libp2p_client:0.1.0:bafybeid3xg5k2ol5adflqloy75ibgljmol6xsvzvezebsg7oudxeeolz7e +contracts: +- valory/service_registry:0.1.0:bafybeieqgcuxmz4uxvlyb62mfsf33qy4xwa5lrij4vvcmrtcsfkng43oyq +protocols: +- valory/contract_api:1.0.0:bafybeidgu7o5llh26xp3u3ebq3yluull5lupiyeu6iooi2xyymdrgnzq5i +- valory/http:1.0.0:bafybeifugzl63kfdmwrxwphrnrhj7bn6iruxieme3a4ntzejf6kmtuwmae +- valory/tendermint:0.1.0:bafybeig4mi3vmlv5zpbjbfuzcgida6j5f2nhrpedxicmrrfjweqc5r7cra +skills: +- valory/abstract_round_abci:0.1.0:bafybeibovsktd3uxur45nrcomq5shcn46cgxd5idmhxbmjhg32c5abyqim +behaviours: + main: + args: {} + class_name: AgentRegistrationRoundBehaviour +handlers: + abci: + args: {} + class_name: ABCIHandler + contract_api: + args: {} + class_name: ContractApiHandler + http: + args: {} + class_name: HttpHandler + ipfs: + args: {} + class_name: IpfsHandler + ledger_api: + args: {} + class_name: LedgerApiHandler + signing: + args: {} + class_name: SigningHandler + tendermint: + args: {} + class_name: TendermintHandler +models: + abci_dialogues: + args: {} + class_name: AbciDialogues + benchmark_tool: + args: + log_dir: /logs + class_name: BenchmarkTool + contract_api_dialogues: + args: {} + class_name: ContractApiDialogues + http_dialogues: + args: {} + class_name: HttpDialogues + ipfs_dialogues: + args: {} + class_name: IpfsDialogues + ledger_api_dialogues: + args: {} + class_name: LedgerApiDialogues + params: + args: + cleanup_history_depth: 1 + cleanup_history_depth_current: null + drand_public_key: 868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31 + genesis_config: + genesis_time: '2022-05-20T16:00:21.735122717Z' + chain_id: chain-c4daS1 + consensus_params: + block: + max_bytes: '22020096' + max_gas: '-1' + time_iota_ms: '1000' + evidence: + max_age_num_blocks: '100000' + max_age_duration: '172800000000000' + max_bytes: '1048576' + validator: + pub_key_types: + - ed25519 + version: {} + voting_power: '10' + keeper_timeout: 30.0 + light_slash_unit_amount: 5000000000000000 + max_attempts: 10 + max_healthcheck: 120 + on_chain_service_id: null + request_retry_delay: 1.0 + request_timeout: 10.0 + reset_pause_duration: 10 + reset_tendermint_after: 2 + retry_attempts: 400 + retry_timeout: 3 + round_timeout_seconds: 30.0 + serious_slash_unit_amount: 8000000000000000 + service_id: registration + service_registry_address: null + setup: + all_participants: + - '0x0000000000000000000000000000000000000000' + safe_contract_address: '0x0000000000000000000000000000000000000000' + consensus_threshold: null + share_tm_config_on_startup: false + slash_cooldown_hours: 3 + slash_threshold_amount: 10000000000000000 + sleep_time: 1 + tendermint_check_sleep_delay: 3 + tendermint_com_url: http://localhost:8080 + tendermint_max_retries: 5 + tendermint_p2p_url: localhost:26656 + tendermint_url: http://localhost:26657 + tx_timeout: 10.0 + use_slashing: false + use_termination: false + class_name: Params + requests: + args: {} + class_name: Requests + signing_dialogues: + args: {} + class_name: SigningDialogues + state: + args: {} + class_name: SharedState + tendermint_dialogues: + args: {} + class_name: TendermintDialogues +dependencies: {} +is_abstract: true +customs: [] diff --git a/packages/valory/skills/registration_abci/tests/__init__.py b/packages/valory/skills/registration_abci/tests/__init__.py new file mode 100644 index 0000000..e4fb2e5 --- /dev/null +++ b/packages/valory/skills/registration_abci/tests/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Tests for valory/registration_abci skill.""" diff --git a/packages/valory/skills/registration_abci/tests/test_behaviours.py b/packages/valory/skills/registration_abci/tests/test_behaviours.py new file mode 100644 index 0000000..1c19c13 --- /dev/null +++ b/packages/valory/skills/registration_abci/tests/test_behaviours.py @@ -0,0 +1,644 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Tests for valory/registration_abci skill's behaviours.""" + +# pylint: skip-file + +import collections +import datetime +import json +import logging +import time +from contextlib import ExitStack, contextmanager +from copy import deepcopy +from pathlib import Path +from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, cast +from unittest import mock +from unittest.mock import MagicMock +from urllib.parse import urlparse + +import pytest +from _pytest.logging import LogCaptureFixture + +from packages.valory.contracts.service_registry.contract import ServiceRegistryContract +from packages.valory.protocols.contract_api.message import ContractApiMessage +from packages.valory.protocols.tendermint.message import TendermintMessage +from packages.valory.skills.abstract_round_abci.base import AbciAppDB +from packages.valory.skills.abstract_round_abci.behaviour_utils import ( + BaseBehaviour, + TimeoutException, + make_degenerate_behaviour, +) +from packages.valory.skills.abstract_round_abci.test_tools.base import ( + FSMBehaviourBaseCase, +) +from packages.valory.skills.registration_abci import PUBLIC_ID +from packages.valory.skills.registration_abci.behaviours import ( + RegistrationBaseBehaviour, + RegistrationBehaviour, + RegistrationStartupBehaviour, +) +from packages.valory.skills.registration_abci.models import SharedState +from packages.valory.skills.registration_abci.rounds import ( + BaseSynchronizedData as RegistrationSynchronizedData, +) +from packages.valory.skills.registration_abci.rounds import ( + Event, + FinishedRegistrationRound, +) + + +PACKAGE_DIR = Path(__file__).parent.parent + + +SERVICE_REGISTRY_ADDRESS = "0xa51c1fc2f0d1a1b8494ed1fe312d7c3a78ed91c0" +ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" +CONTRACT_ID = str(ServiceRegistryContract.contract_id) +ON_CHAIN_SERVICE_ID = 42 +DUMMY_ADDRESS = "localhost" +DUMMY_VALIDATOR_CONFIG = { + "hostname": DUMMY_ADDRESS, + "address": "address", + "pub_key": { + "type": "tendermint/PubKeyEd25519", + "value": "7y7ycBMMABj5Onf74ITYtUS3uZ6SsCQKZML87mIX", + }, + "peer_id": "peer_id", + "p2p_port": 80, +} + + +def test_skill_public_id() -> None: + """Test skill module public ID""" + + assert PUBLIC_ID.name == Path(__file__).parents[1].name + assert PUBLIC_ID.author == Path(__file__).parents[3].name + + +def consume(iterator: Iterable) -> None: + """Consume the iterator""" + collections.deque(iterator, maxlen=0) + + +@contextmanager +def as_context(*contexts: Any) -> Generator[None, None, None]: + """Set contexts""" + with ExitStack() as stack: + consume(map(stack.enter_context, contexts)) + yield + + +class RegistrationAbciBaseCase(FSMBehaviourBaseCase): + """Base case for testing RegistrationAbci FSMBehaviour.""" + + path_to_skill = PACKAGE_DIR + + +class BaseRegistrationTestBehaviour(RegistrationAbciBaseCase): + """Base test case to test RegistrationBehaviour.""" + + behaviour_class = RegistrationBaseBehaviour + next_behaviour_class = BaseBehaviour + + @pytest.mark.parametrize( + "setup_data, expected_initialisation", + ( + ({}, '{"db_data": {"0": {}}, "slashing_config": ""}'), + ({"test": []}, '{"db_data": {"0": {}}, "slashing_config": ""}'), + ( + {"test": [], "valid": [1, 2]}, + '{"db_data": {"0": {"valid": [1, 2]}}, "slashing_config": ""}', + ), + ), + ) + def test_registration( + self, setup_data: Dict, expected_initialisation: Optional[str] + ) -> None: + """Test registration.""" + self.fast_forward_to_behaviour( + self.behaviour, + self.behaviour_class.auto_behaviour_id(), + RegistrationSynchronizedData(AbciAppDB(setup_data=setup_data)), + ) + assert isinstance(self.behaviour.current_behaviour, BaseBehaviour) + assert ( + self.behaviour.current_behaviour.behaviour_id + == self.behaviour_class.auto_behaviour_id() + ) + with mock.patch.object( + self.behaviour.current_behaviour, + "send_a2a_transaction", + side_effect=self.behaviour.current_behaviour.send_a2a_transaction, + ): + self.behaviour.act_wrapper() + assert isinstance( + self.behaviour.current_behaviour.send_a2a_transaction, MagicMock + ) + assert ( + self.behaviour.current_behaviour.send_a2a_transaction.call_args[0][ + 0 + ].initialisation + == expected_initialisation + ) + self.mock_a2a_transaction() + + self._test_done_flag_set() + self.end_round(Event.DONE) + assert ( + self.behaviour.current_behaviour.behaviour_id + == self.next_behaviour_class.auto_behaviour_id() + ) + + +class TestRegistrationStartupBehaviour(RegistrationAbciBaseCase): + """Test case to test RegistrationStartupBehaviour.""" + + behaviour_class = RegistrationStartupBehaviour + next_behaviour_class = make_degenerate_behaviour(FinishedRegistrationRound) + + other_agents: List[str] = ["0xAlice", "0xBob", "0xCharlie"] + _time_in_future = datetime.datetime.now() + datetime.timedelta(hours=10) + _time_in_past = datetime.datetime.now() - datetime.timedelta(hours=10) + + def setup(self, **kwargs: Any) -> None: + """Setup""" + super().setup(**kwargs) + self.state.params.__dict__["sleep_time"] = 0.01 + self.state.params.__dict__["share_tm_config_on_startup"] = True + + def teardown(self, **kwargs: Any) -> None: + """Teardown.""" + super().teardown(**kwargs) + self.state.initial_tm_configs = {} + + @property + def agent_instances(self) -> List[str]: + """Agent instance addresses""" + return [*self.other_agents, self.state.context.agent_address] + + @property + def state(self) -> RegistrationStartupBehaviour: + """Current behavioural state""" + return cast(RegistrationStartupBehaviour, self.behaviour.current_behaviour) + + @property + def logger(self) -> str: + """Logger""" + return "aea.test_agent_name.packages.valory.skills.registration_abci" + + # mock patches + @property + def mocked_service_registry_address(self) -> mock._patch_dict: + """Mocked service registry address""" + return mock.patch.dict( + self.state.params.__dict__, + {"service_registry_address": SERVICE_REGISTRY_ADDRESS}, + ) + + @property + def mocked_on_chain_service_id(self) -> mock._patch_dict: + """Mocked on chain service id""" + return mock.patch.dict( + self.state.params.__dict__, {"on_chain_service_id": ON_CHAIN_SERVICE_ID} + ) + + def mocked_wait_for_condition(self, should_timeout: bool) -> mock._patch: + """Mock BaseBehaviour.wait_for_condition""" + + def dummy_wait_for_condition( + condition: Callable[[], bool], timeout: Optional[float] = None + ) -> Generator[None, None, None]: + """A mock implementation of BaseBehaviour.wait_for_condition""" + # call the condition + condition() + if should_timeout: + # raise in case required + raise TimeoutException() + return + yield + + return mock.patch.object( + self.behaviour.current_behaviour, + "wait_for_condition", + side_effect=dummy_wait_for_condition, + ) + + @property + def mocked_yield_from_sleep(self) -> mock._patch: + """Mock yield from sleep""" + return mock.patch.object(self.behaviour.current_behaviour, "sleep") + + # mock contract calls + def mock_is_correct_contract(self, error_response: bool = False) -> None: + """Mock service registry contract call to for contract verification""" + request_kwargs = dict(performative=ContractApiMessage.Performative.GET_STATE) + state = ContractApiMessage.State(ledger_id="ethereum", body={"verified": True}) + performative = ContractApiMessage.Performative.STATE + if error_response: + performative = ContractApiMessage.Performative.ERROR + response_kwargs = dict( + performative=performative, + callable="verify_contract", + state=state, + ) + self.mock_contract_api_request( + contract_id=CONTRACT_ID, + request_kwargs=request_kwargs, + response_kwargs=response_kwargs, + ) + + def mock_get_agent_instances( + self, *agent_instances: str, error_response: bool = False + ) -> None: + """Mock get agent instances""" + request_kwargs = dict(performative=ContractApiMessage.Performative.GET_STATE) + performative = ContractApiMessage.Performative.STATE + if error_response: + performative = ContractApiMessage.Performative.ERROR + body = {"agentInstances": list(agent_instances)} + state = ContractApiMessage.State(ledger_id="ethereum", body=body) + response_kwargs = dict( + performative=performative, + callable="get_agent_instances", + state=state, + ) + self.mock_contract_api_request( + contract_id=CONTRACT_ID, + request_kwargs=request_kwargs, + response_kwargs=response_kwargs, + ) + + # mock Tendermint config request + def mock_tendermint_request( + self, request_kwargs: Dict, response_kwargs: Dict + ) -> None: + """Mock Tendermint request.""" + + actual_tendermint_message = self.get_message_from_outbox() + assert actual_tendermint_message is not None, "No message in outbox." + has_attributes, error_str = self.message_has_attributes( + actual_message=actual_tendermint_message, + message_type=TendermintMessage, + performative=TendermintMessage.Performative.GET_GENESIS_INFO, + sender=self.state.context.agent_address, + to=actual_tendermint_message.to, + **request_kwargs, + ) + assert has_attributes, error_str + incoming_message = self.build_incoming_message( + message_type=TendermintMessage, + dialogue_reference=( + actual_tendermint_message.dialogue_reference[0], + "stub", + ), + performative=TendermintMessage.Performative.GENESIS_INFO, + target=actual_tendermint_message.message_id, + message_id=-1, + to=self.state.context.agent_address, + sender=actual_tendermint_message.to, + **response_kwargs, + ) + self.tendermint_handler.handle(cast(TendermintMessage, incoming_message)) + + def mock_get_tendermint_info(self, *addresses: str) -> None: + """Mock get Tendermint info""" + for i in addresses: + request_kwargs: Dict = dict() + config = deepcopy(DUMMY_VALIDATOR_CONFIG) + config["address"] = str(config["address"]) + i + info = json.dumps(config) + response_kwargs = dict(info=info) + self.mock_tendermint_request(request_kwargs, response_kwargs) + # give room to the behaviour to finish sleeping + # using the same sleep time here and in the behaviour + # can lead to problems when the sleep here is finished + # before the one in the behaviour + time.sleep(self.state.params.sleep_time * 2) + self.behaviour.act_wrapper() + + # mock HTTP requests + def mock_get_local_tendermint_params(self, valid_response: bool = True) -> None: + """Mock Tendermint get local params""" + url = self.state.tendermint_parameter_url + request_kwargs = dict(method="GET", url=url) + body = b"" + if valid_response: + params = dict(params=DUMMY_VALIDATOR_CONFIG, status=True, error=None) + body = json.dumps(params).encode(self.state.ENCODING) + response_kwargs = dict(status_code=200, body=body) + self.mock_http_request(request_kwargs, response_kwargs) + + def mock_tendermint_update(self, valid_response: bool = True) -> None: + """Mock Tendermint update""" + + validator_configs = self.state.format_genesis_data( + self.state.initial_tm_configs + ) + body = json.dumps(validator_configs).encode(self.state.ENCODING) + url = self.state.tendermint_parameter_url + request_kwargs = dict(method="POST", url=url, body=body) + body = ( + json.dumps({"status": True, "error": None}).encode(self.state.ENCODING) + if valid_response + else b"" + ) + response_kwargs = dict(status_code=200, body=body) + self.mock_http_request(request_kwargs, response_kwargs) + + def set_last_timestamp(self, last_timestamp: Optional[datetime.datetime]) -> None: + """Set last timestamp""" + if last_timestamp is not None: + state = cast(SharedState, self._skill.skill_context.state) + state.round_sequence.blockchain._blocks.append( + MagicMock(timestamp=last_timestamp) + ) + + @staticmethod + def dummy_reset_tendermint_with_wait_wrapper( + valid_response: bool, + ) -> Callable[[], Generator[None, None, Optional[bool]]]: + """Wrapper for a Dummy `reset_tendermint_with_wait` method.""" + + def dummy_reset_tendermint_with_wait( + **_: bool, + ) -> Generator[None, None, Optional[bool]]: + """Dummy `reset_tendermint_with_wait` method.""" + yield + return valid_response + + return dummy_reset_tendermint_with_wait + + # tests + def test_init(self) -> None: + """Empty init""" + assert self.state.initial_tm_configs == {} + assert self.state.local_tendermint_params == {} + assert self.state.updated_genesis_data == {} + + def test_no_contract_address(self, caplog: LogCaptureFixture) -> None: + """Test service registry contract address not provided""" + with as_context( + caplog.at_level(logging.INFO, logger=self.logger), + self.mocked_yield_from_sleep, + ): + self.behaviour.act_wrapper() + self.mock_get_local_tendermint_params() + log_message = self.state.LogMessages.no_contract_address + assert log_message.value in caplog.text + + @pytest.mark.parametrize("valid_response", [True, False]) + def test_request_personal( + self, valid_response: bool, caplog: LogCaptureFixture + ) -> None: + """Test get tendermint configuration""" + + failed_message = self.state.LogMessages.failed_personal + response_message = self.state.LogMessages.response_personal + log_message = [failed_message, response_message][valid_response] + with as_context( + caplog.at_level(logging.INFO, logger=self.logger), + self.mocked_service_registry_address, + self.mocked_yield_from_sleep, + ): + self.behaviour.act_wrapper() + self.mock_get_local_tendermint_params(valid_response=valid_response) + assert log_message.value in caplog.text + + def test_failed_verification(self, caplog: LogCaptureFixture) -> None: + """Test service registry contract not correctly deployed""" + + with as_context( + caplog.at_level(logging.INFO, logger=self.logger), + self.mocked_service_registry_address, + ): + self.behaviour.act_wrapper() + self.mock_get_local_tendermint_params() + self.mock_is_correct_contract(error_response=True) + log_message = self.state.LogMessages.failed_verification + assert log_message.value in caplog.text + + def test_on_chain_service_id_not_set(self, caplog: LogCaptureFixture) -> None: + """Test `get_addresses` when `on_chain_service_id` is `None`.""" + + with as_context( + caplog.at_level(logging.INFO, logger=self.logger), + self.mocked_service_registry_address, + ): + self.behaviour.act_wrapper() + self.mock_get_local_tendermint_params() + self.mock_is_correct_contract() + log_message = self.state.LogMessages.no_on_chain_service_id + assert log_message.value in caplog.text + + def test_failed_service_info(self, caplog: LogCaptureFixture) -> None: + """Test get service info failure""" + + with as_context( + caplog.at_level(logging.INFO, logger=self.logger), + self.mocked_service_registry_address, + self.mocked_on_chain_service_id, + ): + self.behaviour.act_wrapper() + self.mock_get_local_tendermint_params() + self.mock_is_correct_contract() + self.mock_get_agent_instances(error_response=True) + log_message = self.state.LogMessages.failed_service_info + assert log_message.value in caplog.text + + def test_no_agents_registered(self, caplog: LogCaptureFixture) -> None: + """Test no agent instances registered""" + + with as_context( + caplog.at_level(logging.INFO, logger=self.logger), + self.mocked_service_registry_address, + self.mocked_on_chain_service_id, + ): + self.behaviour.act_wrapper() + self.mock_get_local_tendermint_params() + self.mock_is_correct_contract() + self.mock_get_agent_instances() + log_message = self.state.LogMessages.no_agents_registered + assert log_message.value in caplog.text + + def test_self_not_registered(self, caplog: LogCaptureFixture) -> None: + """Test node operator agent not registered""" + + with as_context( + caplog.at_level(logging.INFO, logger=self.logger), + self.mocked_service_registry_address, + self.mocked_on_chain_service_id, + ): + self.behaviour.act_wrapper() + self.mock_get_local_tendermint_params() + self.mock_is_correct_contract() + self.mock_get_agent_instances(*self.other_agents) + log_message = self.state.LogMessages.self_not_registered + assert log_message.value in caplog.text + + def test_response_service_info(self, caplog: LogCaptureFixture) -> None: + """Test registered addresses retrieved""" + + with as_context( + caplog.at_level(logging.INFO, logger=self.logger), + self.mocked_service_registry_address, + self.mocked_on_chain_service_id, + ): + self.behaviour.act_wrapper() + self.mock_get_local_tendermint_params() + self.mock_is_correct_contract() + self.mock_get_agent_instances(*self.agent_instances) + + assert set(self.state.initial_tm_configs) == set(self.agent_instances) + my_info = self.state.initial_tm_configs[self.state.context.agent_address] + assert ( + my_info["hostname"] + == urlparse(self.state.context.params.tendermint_url).hostname + ) + assert not any(map(self.state.initial_tm_configs.get, self.other_agents)) + log_message = self.state.LogMessages.response_service_info + assert log_message.value in caplog.text + + def test_collection_complete(self, caplog: LogCaptureFixture) -> None: + """Test registered addresses retrieved""" + + with as_context( + caplog.at_level(logging.INFO, logger=self.logger), + self.mocked_service_registry_address, + self.mocked_on_chain_service_id, + mock.patch.object( + self._skill.skill_context.state, + "acn_container", + side_effect=lambda: self.agent_instances, + ), + ): + self.behaviour.act_wrapper() + self.mock_get_local_tendermint_params() + self.mock_is_correct_contract() + self.mock_get_agent_instances(*self.agent_instances) + self.mock_get_tendermint_info(*self.other_agents) + + initial_tm_configs = self.state.initial_tm_configs + validator_to_agent = ( + self.state.context.state.round_sequence.validator_to_agent + ) + + assert all(map(initial_tm_configs.get, self.other_agents)) + assert tuple(validator_to_agent.keys()) == tuple( + config["address"] for config in initial_tm_configs.values() + ) + assert tuple(validator_to_agent.values()) == tuple( + initial_tm_configs.keys() + ) + log_message = self.state.LogMessages.collection_complete + assert log_message.value in caplog.text + + @pytest.mark.parametrize("valid_response", [True, False]) + @mock.patch.object(BaseBehaviour, "reset_tendermint_with_wait") + def test_request_update( + self, _: mock.Mock, valid_response: bool, caplog: LogCaptureFixture + ) -> None: + """Test Tendermint config update""" + + self.state.updated_genesis_data = {} + failed_message = self.state.LogMessages.failed_update + response_message = self.state.LogMessages.response_update + log_message = [failed_message, response_message][valid_response] + with as_context( + caplog.at_level(logging.INFO, logger=self.logger), + self.mocked_service_registry_address, + self.mocked_on_chain_service_id, + self.mocked_yield_from_sleep, + mock.patch.object( + self._skill.skill_context.state, + "acn_container", + side_effect=lambda: self.agent_instances, + ), + ): + self.behaviour.act_wrapper() + self.mock_get_local_tendermint_params() + self.mock_is_correct_contract() + self.mock_get_agent_instances(*self.agent_instances) + self.mock_get_tendermint_info(*self.other_agents) + self.mock_tendermint_update(valid_response) + assert log_message.value in caplog.text + + @pytest.mark.parametrize( + "valid_response, last_timestamp, timeout", + [ + (True, _time_in_past, False), + (False, _time_in_past, False), + (True, _time_in_future, False), + (False, _time_in_future, False), + (True, None, False), + (False, None, False), + (True, None, True), + (False, None, True), + ], + ) + def test_request_restart( + self, + valid_response: bool, + last_timestamp: Optional[datetime.datetime], + timeout: bool, + caplog: LogCaptureFixture, + ) -> None: + """Test Tendermint start""" + self.state.updated_genesis_data = {} + self.set_last_timestamp(last_timestamp) + with as_context( + caplog.at_level(logging.INFO, logger=self.logger), + self.mocked_service_registry_address, + self.mocked_on_chain_service_id, + self.mocked_wait_for_condition(timeout), + self.mocked_yield_from_sleep, + mock.patch.object( + self.behaviour.current_behaviour, + "reset_tendermint_with_wait", + side_effect=self.dummy_reset_tendermint_with_wait_wrapper( + valid_response + ), + ), + mock.patch.object( + self._skill.skill_context.state, + "acn_container", + side_effect=lambda: self.agent_instances, + ), + ): + self.behaviour.act_wrapper() + self.mock_get_local_tendermint_params() + self.mock_is_correct_contract() + self.mock_get_agent_instances(*self.agent_instances) + self.mock_get_tendermint_info(*self.other_agents) + self.mock_tendermint_update() + self.behaviour.act_wrapper() + + +class TestRegistrationStartupBehaviourNoConfigShare(BaseRegistrationTestBehaviour): + """Test case to test RegistrationBehaviour.""" + + behaviour_class = RegistrationStartupBehaviour + next_behaviour_class = make_degenerate_behaviour(FinishedRegistrationRound) + + +class TestRegistrationBehaviour(BaseRegistrationTestBehaviour): + """Test case to test RegistrationBehaviour.""" + + behaviour_class = RegistrationBehaviour + next_behaviour_class = make_degenerate_behaviour(FinishedRegistrationRound) diff --git a/packages/valory/skills/registration_abci/tests/test_dialogues.py b/packages/valory/skills/registration_abci/tests/test_dialogues.py new file mode 100644 index 0000000..55d61a5 --- /dev/null +++ b/packages/valory/skills/registration_abci/tests/test_dialogues.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test the dialogues.py module of the skill.""" + +# pylint: skip-file + +import packages.valory.skills.registration_abci.dialogues # noqa + + +def test_import() -> None: + """Test that the 'dialogues.py' Python module can be imported.""" diff --git a/packages/valory/skills/registration_abci/tests/test_handlers.py b/packages/valory/skills/registration_abci/tests/test_handlers.py new file mode 100644 index 0000000..aae35f0 --- /dev/null +++ b/packages/valory/skills/registration_abci/tests/test_handlers.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test the dialogues.py module of the skill.""" + +# pylint: skip-file + +import packages.valory.skills.registration_abci.handlers # noqa + + +def test_import() -> None: + """Test that the 'handlers.py' Python module can be imported.""" diff --git a/packages/valory/skills/registration_abci/tests/test_models.py b/packages/valory/skills/registration_abci/tests/test_models.py new file mode 100644 index 0000000..eb6d4d7 --- /dev/null +++ b/packages/valory/skills/registration_abci/tests/test_models.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test the models.py module of the skill.""" + +# pylint: skip-file + +from packages.valory.skills.abstract_round_abci.test_tools.base import DummyContext +from packages.valory.skills.registration_abci.models import SharedState + + +class TestSharedState: + """Test SharedState(Model) class.""" + + def test_initialization( + self, + ) -> None: + """Test initialization.""" + SharedState(name="", skill_context=DummyContext()) diff --git a/packages/valory/skills/registration_abci/tests/test_payloads.py b/packages/valory/skills/registration_abci/tests/test_payloads.py new file mode 100644 index 0000000..b9b0a40 --- /dev/null +++ b/packages/valory/skills/registration_abci/tests/test_payloads.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test the payloads.py module of the skill.""" + +# pylint: skip-file + +import pytest + +from packages.valory.skills.abstract_round_abci.base import Transaction +from packages.valory.skills.registration_abci.payloads import RegistrationPayload + + +def test_registration_abci_payload() -> None: + """Test `RegistrationPayload`.""" + + payload = RegistrationPayload(sender="sender", initialisation="dummy") + + assert payload.initialisation == "dummy" + assert payload.data == {"initialisation": "dummy"} + assert RegistrationPayload.from_json(payload.json) == payload + + +def test_registration_abci_payload_raises() -> None: + """Test `RegistrationPayload`.""" + payload = RegistrationPayload(sender="sender", initialisation="0" * 10**7) + signature = "signature" + tx = Transaction(payload, signature) + with pytest.raises(ValueError, match="Transaction must be smaller"): + tx.encode() diff --git a/packages/valory/skills/registration_abci/tests/test_rounds.py b/packages/valory/skills/registration_abci/tests/test_rounds.py new file mode 100644 index 0000000..d442800 --- /dev/null +++ b/packages/valory/skills/registration_abci/tests/test_rounds.py @@ -0,0 +1,371 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test the rounds.py module of the skill.""" + +import json +from typing import Any, Dict, Optional, cast +from unittest import mock +from unittest.mock import MagicMock, PropertyMock + +import pytest + +from packages.valory.skills.abstract_round_abci.base import AbciAppDB +from packages.valory.skills.abstract_round_abci.base import ( + BaseSynchronizedData as SynchronizedData, +) +from packages.valory.skills.abstract_round_abci.base import ( + CollectSameUntilAllRound, + SlashingNotConfiguredError, +) +from packages.valory.skills.abstract_round_abci.test_tools.rounds import ( + BaseCollectSameUntilAllRoundTest, + BaseCollectSameUntilThresholdRoundTest, +) +from packages.valory.skills.registration_abci.payloads import RegistrationPayload +from packages.valory.skills.registration_abci.rounds import Event as RegistrationEvent +from packages.valory.skills.registration_abci.rounds import ( + RegistrationRound, + RegistrationStartupRound, +) + + +# pylint: skip-file + + +class TestRegistrationStartupRound(BaseCollectSameUntilAllRoundTest): + """Test RegistrationStartupRound.""" + + _synchronized_data_class = SynchronizedData + _event_class = RegistrationEvent + + @pytest.mark.parametrize("slashing_config", ("", json.dumps({"valid": "config"}))) + def test_run_default( + self, + slashing_config: str, + ) -> None: + """Run test.""" + + self.synchronized_data = cast( + SynchronizedData, + self.synchronized_data.update( + safe_contract_address="stub_safe_contract_address", + oracle_contract_address="stub_oracle_contract_address", + ), + ) + + test_round = RegistrationStartupRound( + synchronized_data=self.synchronized_data, + context=MagicMock(), + ) + + self.synchronized_data.slashing_config = slashing_config + + if not slashing_config: + seq = test_round.context.state.round_sequence + type(seq).offence_status = PropertyMock( + side_effect=SlashingNotConfiguredError + ) + + most_voted_payload = self.synchronized_data.db.serialize() + round_payloads = { + participant: RegistrationPayload( + sender=participant, + initialisation=most_voted_payload, + ) + for participant in self.participants + } + + self._run_with_round( + test_round, + round_payloads, + most_voted_payload, + RegistrationEvent.DONE, + ) + + assert all( + ( + self.synchronized_data.all_participants + == frozenset({"agent_2", "agent_0", "agent_1", "agent_3"}), + self.synchronized_data.participants + == frozenset({"agent_2", "agent_0", "agent_1", "agent_3"}), + self.synchronized_data.safe_contract_address + == "stub_safe_contract_address", + self.synchronized_data.db.get("oracle_contract_address") + == "stub_oracle_contract_address", + ) + ) + + test_round.context.state.round_sequence.sync_db_and_slashing.assert_called_once_with( + most_voted_payload + ) + + if slashing_config: + test_round.context.state.round_sequence.enable_slashing.assert_called_once() + else: + test_round.context.state.round_sequence.enable_slashing.assert_not_called() + + def test_run_default_not_finished( + self, + ) -> None: + """Run test.""" + + self.synchronized_data = cast( + SynchronizedData, + self.synchronized_data.update( + safe_contract_address="stub_safe_contract_address", + oracle_contract_address="stub_oracle_contract_address", + ), + ) + test_round = RegistrationStartupRound( + synchronized_data=self.synchronized_data, + context=MagicMock(), + ) + + with mock.patch.object( + CollectSameUntilAllRound, + "collection_threshold_reached", + new_callable=mock.PropertyMock, + ) as threshold_mock: + threshold_mock.return_value = False + self._run_with_round( + test_round, + finished=False, + most_voted_payload="none", + ) + + assert all( + ( + self.synchronized_data.all_participants + == frozenset({"agent_2", "agent_0", "agent_1", "agent_3"}), + self.synchronized_data.participants + == frozenset({"agent_2", "agent_0", "agent_1", "agent_3"}), + self.synchronized_data.safe_contract_address + == "stub_safe_contract_address", + self.synchronized_data.db.get("oracle_contract_address") + == "stub_oracle_contract_address", + ) + ) + + def _run_with_round( + self, + test_round: RegistrationStartupRound, + round_payloads: Optional[Dict[str, RegistrationPayload]] = None, + most_voted_payload: Optional[Any] = None, + expected_event: Optional[RegistrationEvent] = None, + finished: bool = True, + ) -> None: + """Run with given round.""" + + round_payloads = round_payloads or { + p: RegistrationPayload(sender=p, initialisation="none") + for p in self.participants + } + + test_runner = self._test_round( + test_round=test_round, + round_payloads=round_payloads, + synchronized_data_update_fn=( + lambda *x: SynchronizedData( + AbciAppDB( + setup_data=dict(participants=[tuple(self.participants)]), + ) + ) + ), + synchronized_data_attr_checks=[ + lambda _synchronized_data: _synchronized_data.participants + ], + most_voted_payload=most_voted_payload, + exit_event=expected_event, + finished=finished, + ) + + next(test_runner) + next(test_runner) + next(test_runner) + if finished: + next(test_runner) + + +class TestRegistrationRound(BaseCollectSameUntilThresholdRoundTest): + """Test RegistrationRound.""" + + _synchronized_data_class = SynchronizedData + _event_class = RegistrationEvent + + def test_run_default( + self, + ) -> None: + """Run test.""" + self.synchronized_data = cast( + SynchronizedData, + self.synchronized_data.update( + safe_contract_address="stub_safe_contract_address", + oracle_contract_address="stub_oracle_contract_address", + ), + ) + test_round = RegistrationRound( + synchronized_data=self.synchronized_data, + context=MagicMock(), + ) + + payload_data = self.synchronized_data.db.serialize() + + round_payloads = { + participant: RegistrationPayload( + sender=participant, + initialisation=payload_data, + ) + for participant in self.participants + } + + self._run_with_round( + test_round=test_round, + expected_event=RegistrationEvent.DONE, + confirmations=11, + most_voted_payload=payload_data, + round_payloads=round_payloads, + ) + + assert all( + ( + self.synchronized_data.all_participants + == frozenset({"agent_2", "agent_0", "agent_1", "agent_3"}), + self.synchronized_data.participants + == frozenset({"agent_2", "agent_0", "agent_1", "agent_3"}), + self.synchronized_data.safe_contract_address + == "stub_safe_contract_address", + self.synchronized_data.db.get("oracle_contract_address") + == "stub_oracle_contract_address", + ) + ) + + def test_run_default_not_finished( + self, + ) -> None: + """Run test.""" + self.synchronized_data = cast( + SynchronizedData, + self.synchronized_data.update( + safe_contract_address="stub_safe_contract_address", + oracle_contract_address="stub_oracle_contract_address", + ), + ) + test_round = RegistrationRound( + synchronized_data=self.synchronized_data, + context=MagicMock(), + ) + self._run_with_round( + test_round, + confirmations=None, + ) + + assert all( + ( + self.synchronized_data.all_participants + == frozenset({"agent_2", "agent_0", "agent_1", "agent_3"}), + self.synchronized_data.participants + == frozenset({"agent_2", "agent_0", "agent_1", "agent_3"}), + self.synchronized_data.safe_contract_address + == "stub_safe_contract_address", + self.synchronized_data.db.get("oracle_contract_address") + == "stub_oracle_contract_address", + ) + ) + + def _run_with_round( + self, + test_round: RegistrationRound, + round_payloads: Optional[Dict[str, RegistrationPayload]] = None, + most_voted_payload: Optional[Any] = None, + expected_event: Optional[RegistrationEvent] = None, + confirmations: Optional[int] = None, + finished: bool = True, + ) -> None: + """Run with given round.""" + + round_payloads = round_payloads or { + p: RegistrationPayload(sender=p, initialisation="none") + for p in self.participants + } + + test_runner = self._test_round( + test_round=test_round, + round_payloads=round_payloads, + synchronized_data_update_fn=( + lambda *x: SynchronizedData( + AbciAppDB( + setup_data=dict(participants=[tuple(self.participants)]), + ) + ) + ), + synchronized_data_attr_checks=[ + lambda _synchronized_data: _synchronized_data.participants + ], + most_voted_payload=most_voted_payload, + exit_event=expected_event, + ) + + next(test_runner) + if confirmations is None: + assert ( + test_round.block_confirmations + <= test_round.required_block_confirmations + ) + + else: + test_round.block_confirmations = confirmations + test_round = next(test_runner) + prior_confirmations = test_round.block_confirmations + next(test_runner) + assert test_round.block_confirmations == prior_confirmations + 1 + if finished: + next(test_runner) + + def test_no_majority(self) -> None: + """Test the NO_MAJORITY event.""" + self.synchronized_data = cast( + SynchronizedData, + self.synchronized_data.update( + safe_contract_address="stub_safe_contract_address", + oracle_contract_address="stub_oracle_contract_address", + ), + ) + + test_round = RegistrationRound( + synchronized_data=self.synchronized_data, + context=MagicMock(), + ) + + with mock.patch.object(test_round, "is_majority_possible", return_value=False): + with mock.patch.object(test_round, "block_confirmations", 11): + self._test_no_majority_event(test_round) + + assert all( + ( + self.synchronized_data.all_participants + == frozenset({"agent_2", "agent_0", "agent_1", "agent_3"}), + self.synchronized_data.participants + == frozenset({"agent_2", "agent_0", "agent_1", "agent_3"}), + self.synchronized_data.safe_contract_address + == "stub_safe_contract_address", + self.synchronized_data.db.get("oracle_contract_address") + == "stub_oracle_contract_address", + ) + ) diff --git a/packages/valory/skills/reset_pause_abci/README.md b/packages/valory/skills/reset_pause_abci/README.md new file mode 100644 index 0000000..99ca798 --- /dev/null +++ b/packages/valory/skills/reset_pause_abci/README.md @@ -0,0 +1,23 @@ +# Reset and pause abci + +## Description + +This module contains the ABCI reset and pause skill for an AEA. It implements an ABCI +application. + +## Behaviours + +* `ResetAndPauseBehaviour` + + Reset state. + +* `ResetPauseABCIConsensusBehaviour` + + This behaviour manages the consensus stages for the reset and pause abci app. + +## Handlers + +* `ResetPauseABCIHandler` +* `HttpHandler` +* `SigningHandler` + diff --git a/packages/valory/skills/reset_pause_abci/__init__.py b/packages/valory/skills/reset_pause_abci/__init__.py new file mode 100644 index 0000000..850ef39 --- /dev/null +++ b/packages/valory/skills/reset_pause_abci/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the Reset & Pause skill for an AEA.""" + +from aea.configurations.base import PublicId + + +PUBLIC_ID = PublicId.from_str("valory/reset_pause_abci:0.1.0") diff --git a/packages/valory/skills/reset_pause_abci/behaviours.py b/packages/valory/skills/reset_pause_abci/behaviours.py new file mode 100644 index 0000000..9ee30a1 --- /dev/null +++ b/packages/valory/skills/reset_pause_abci/behaviours.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the behaviours for the 'reset_pause_abci' skill.""" + +from abc import ABC +from typing import Generator, Set, Type, cast + +from packages.valory.skills.abstract_round_abci.base import BaseSynchronizedData +from packages.valory.skills.abstract_round_abci.behaviours import ( + AbstractRoundBehaviour, + BaseBehaviour, +) +from packages.valory.skills.reset_pause_abci.models import Params, SharedState +from packages.valory.skills.reset_pause_abci.payloads import ResetPausePayload +from packages.valory.skills.reset_pause_abci.rounds import ( + ResetAndPauseRound, + ResetPauseAbciApp, +) + + +class ResetAndPauseBaseBehaviour(BaseBehaviour, ABC): + """Reset behaviour.""" + + @property + def synchronized_data(self) -> BaseSynchronizedData: + """Return the synchronized data.""" + return cast( + BaseSynchronizedData, + cast(SharedState, self.context.state).synchronized_data, + ) + + @property + def params(self) -> Params: + """Return the params.""" + return cast(Params, self.context.params) + + +class ResetAndPauseBehaviour(ResetAndPauseBaseBehaviour): + """Reset and pause behaviour.""" + + matching_round = ResetAndPauseRound + + def async_act(self) -> Generator: + """ + Do the action. + + Steps: + - Trivially log the behaviour. + - Sleep for configured interval. + - Build a registration transaction. + - Send the transaction and wait for it to be mined. + - Wait until ABCI application transitions to the next round. + - Go to the next behaviour (set done event). + """ + # + 1 because `period_count` starts from 0 + n_periods_done = self.synchronized_data.period_count + 1 + reset_tm_nodes = n_periods_done % self.params.reset_tendermint_after == 0 + if reset_tm_nodes: + tendermint_reset = yield from self.reset_tendermint_with_wait() + if not tendermint_reset: + return + else: + yield from self.wait_from_last_timestamp(self.params.reset_pause_duration) + self.context.logger.info("Period end.") + self.context.benchmark_tool.save(self.synchronized_data.period_count) + + payload = ResetPausePayload( + self.context.agent_address, self.synchronized_data.period_count + ) + yield from self.send_a2a_transaction(payload, reset_tm_nodes) + yield from self.wait_until_round_end() + self.set_done() + + +class ResetPauseABCIConsensusBehaviour(AbstractRoundBehaviour): + """This behaviour manages the consensus stages for the reset_pause_abci app.""" + + initial_behaviour_cls = ResetAndPauseBehaviour + abci_app_cls = ResetPauseAbciApp + behaviours: Set[Type[BaseBehaviour]] = { + ResetAndPauseBehaviour, # type: ignore + } diff --git a/packages/valory/skills/reset_pause_abci/dialogues.py b/packages/valory/skills/reset_pause_abci/dialogues.py new file mode 100644 index 0000000..e244bde --- /dev/null +++ b/packages/valory/skills/reset_pause_abci/dialogues.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the classes required for dialogue management.""" + +from packages.valory.skills.abstract_round_abci.dialogues import ( + AbciDialogue as BaseAbciDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + AbciDialogues as BaseAbciDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + ContractApiDialogue as BaseContractApiDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + ContractApiDialogues as BaseContractApiDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + HttpDialogue as BaseHttpDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + HttpDialogues as BaseHttpDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + IpfsDialogue as BaseIpfsDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + IpfsDialogues as BaseIpfsDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + LedgerApiDialogue as BaseLedgerApiDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + LedgerApiDialogues as BaseLedgerApiDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + SigningDialogue as BaseSigningDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + SigningDialogues as BaseSigningDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + TendermintDialogue as BaseTendermintDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + TendermintDialogues as BaseTendermintDialogues, +) + + +AbciDialogue = BaseAbciDialogue +AbciDialogues = BaseAbciDialogues + + +HttpDialogue = BaseHttpDialogue +HttpDialogues = BaseHttpDialogues + + +SigningDialogue = BaseSigningDialogue +SigningDialogues = BaseSigningDialogues + + +LedgerApiDialogue = BaseLedgerApiDialogue +LedgerApiDialogues = BaseLedgerApiDialogues + + +ContractApiDialogue = BaseContractApiDialogue +ContractApiDialogues = BaseContractApiDialogues + + +TendermintDialogue = BaseTendermintDialogue +TendermintDialogues = BaseTendermintDialogues + + +IpfsDialogue = BaseIpfsDialogue +IpfsDialogues = BaseIpfsDialogues diff --git a/packages/valory/skills/reset_pause_abci/fsm_specification.yaml b/packages/valory/skills/reset_pause_abci/fsm_specification.yaml new file mode 100644 index 0000000..b4882ef --- /dev/null +++ b/packages/valory/skills/reset_pause_abci/fsm_specification.yaml @@ -0,0 +1,19 @@ +alphabet_in: +- DONE +- NO_MAJORITY +- RESET_AND_PAUSE_TIMEOUT +default_start_state: ResetAndPauseRound +final_states: +- FinishedResetAndPauseErrorRound +- FinishedResetAndPauseRound +label: ResetPauseAbciApp +start_states: +- ResetAndPauseRound +states: +- FinishedResetAndPauseErrorRound +- FinishedResetAndPauseRound +- ResetAndPauseRound +transition_func: + (ResetAndPauseRound, DONE): FinishedResetAndPauseRound + (ResetAndPauseRound, NO_MAJORITY): FinishedResetAndPauseErrorRound + (ResetAndPauseRound, RESET_AND_PAUSE_TIMEOUT): FinishedResetAndPauseErrorRound diff --git a/packages/valory/skills/reset_pause_abci/handlers.py b/packages/valory/skills/reset_pause_abci/handlers.py new file mode 100644 index 0000000..d6c6335 --- /dev/null +++ b/packages/valory/skills/reset_pause_abci/handlers.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the handler for the 'reset_pause_abci' skill.""" + +from packages.valory.skills.abstract_round_abci.handlers import ( + ABCIRoundHandler as BaseABCIRoundHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + ContractApiHandler as BaseContractApiHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + HttpHandler as BaseHttpHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + IpfsHandler as BaseIpfsHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + LedgerApiHandler as BaseLedgerApiHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + SigningHandler as BaseSigningHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + TendermintHandler as BaseTendermintHandler, +) + + +ABCIHandler = BaseABCIRoundHandler +HttpHandler = BaseHttpHandler +SigningHandler = BaseSigningHandler +LedgerApiHandler = BaseLedgerApiHandler +ContractApiHandler = BaseContractApiHandler +TendermintHandler = BaseTendermintHandler +IpfsHandler = BaseIpfsHandler diff --git a/packages/valory/skills/reset_pause_abci/models.py b/packages/valory/skills/reset_pause_abci/models.py new file mode 100644 index 0000000..89d44fc --- /dev/null +++ b/packages/valory/skills/reset_pause_abci/models.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the shared state for the 'reset_pause_abci' application.""" + +from packages.valory.skills.abstract_round_abci.models import BaseParams +from packages.valory.skills.abstract_round_abci.models import ( + BenchmarkTool as BaseBenchmarkTool, +) +from packages.valory.skills.abstract_round_abci.models import Requests as BaseRequests +from packages.valory.skills.abstract_round_abci.models import ( + SharedState as BaseSharedState, +) +from packages.valory.skills.reset_pause_abci.rounds import Event, ResetPauseAbciApp + + +MARGIN = 5 + +Requests = BaseRequests +BenchmarkTool = BaseBenchmarkTool + + +class SharedState(BaseSharedState): + """Keep the current shared state of the skill.""" + + abci_app_cls = ResetPauseAbciApp + + def setup(self) -> None: + """Set up.""" + super().setup() + ResetPauseAbciApp.event_to_timeout[ + Event.ROUND_TIMEOUT + ] = self.context.params.round_timeout_seconds + ResetPauseAbciApp.event_to_timeout[Event.RESET_AND_PAUSE_TIMEOUT] = ( + self.context.params.reset_pause_duration + MARGIN + ) + + +Params = BaseParams diff --git a/packages/valory/skills/reset_pause_abci/payloads.py b/packages/valory/skills/reset_pause_abci/payloads.py new file mode 100644 index 0000000..d5c0058 --- /dev/null +++ b/packages/valory/skills/reset_pause_abci/payloads.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the transaction payloads for the reset_pause_abci app.""" + +from dataclasses import dataclass + +from packages.valory.skills.abstract_round_abci.base import BaseTxPayload + + +@dataclass(frozen=True) +class ResetPausePayload(BaseTxPayload): + """Represent a transaction payload of type 'reset'.""" + + period_count: int diff --git a/packages/valory/skills/reset_pause_abci/rounds.py b/packages/valory/skills/reset_pause_abci/rounds.py new file mode 100644 index 0000000..1fd666a --- /dev/null +++ b/packages/valory/skills/reset_pause_abci/rounds.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the data classes for the reset_pause_abci application.""" + +from enum import Enum +from typing import Dict, Optional, Set, Tuple + +from packages.valory.skills.abstract_round_abci.base import ( + AbciApp, + AbciAppTransitionFunction, + AppState, + BaseSynchronizedData, + CollectSameUntilThresholdRound, + DegenerateRound, +) +from packages.valory.skills.reset_pause_abci.payloads import ResetPausePayload + + +class Event(Enum): + """Event enumeration for the reset_pause_abci app.""" + + DONE = "done" + ROUND_TIMEOUT = "round_timeout" + NO_MAJORITY = "no_majority" + RESET_AND_PAUSE_TIMEOUT = "reset_and_pause_timeout" + + +class ResetAndPauseRound(CollectSameUntilThresholdRound): + """A round that represents that consensus is reached (the final round)""" + + payload_class = ResetPausePayload + _allow_rejoin_payloads = True + synchronized_data_class = BaseSynchronizedData + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: + """Process the end of the block.""" + if self.threshold_reached: + return self.synchronized_data.create(), Event.DONE + if not self.is_majority_possible( + self.collection, self.synchronized_data.nb_participants + ): + return self.synchronized_data, Event.NO_MAJORITY + return None + + +class FinishedResetAndPauseRound(DegenerateRound): + """A round that represents reset and pause has finished""" + + +class FinishedResetAndPauseErrorRound(DegenerateRound): + """A round that represents reset and pause has finished with errors""" + + +class ResetPauseAbciApp(AbciApp[Event]): + """ResetPauseAbciApp + + Initial round: ResetAndPauseRound + + Initial states: {ResetAndPauseRound} + + Transition states: + 0. ResetAndPauseRound + - done: 1. + - reset and pause timeout: 2. + - no majority: 2. + 1. FinishedResetAndPauseRound + 2. FinishedResetAndPauseErrorRound + + Final states: {FinishedResetAndPauseErrorRound, FinishedResetAndPauseRound} + + Timeouts: + round timeout: 30.0 + reset and pause timeout: 30.0 + """ + + initial_round_cls: AppState = ResetAndPauseRound + transition_function: AbciAppTransitionFunction = { + ResetAndPauseRound: { + Event.DONE: FinishedResetAndPauseRound, + Event.RESET_AND_PAUSE_TIMEOUT: FinishedResetAndPauseErrorRound, + Event.NO_MAJORITY: FinishedResetAndPauseErrorRound, + }, + FinishedResetAndPauseRound: {}, + FinishedResetAndPauseErrorRound: {}, + } + final_states: Set[AppState] = { + FinishedResetAndPauseRound, + FinishedResetAndPauseErrorRound, + } + event_to_timeout: Dict[Event, float] = { + Event.ROUND_TIMEOUT: 30.0, + Event.RESET_AND_PAUSE_TIMEOUT: 30.0, + } + db_pre_conditions: Dict[AppState, Set[str]] = {ResetAndPauseRound: set()} + db_post_conditions: Dict[AppState, Set[str]] = { + FinishedResetAndPauseRound: set(), + FinishedResetAndPauseErrorRound: set(), + } diff --git a/packages/valory/skills/reset_pause_abci/skill.yaml b/packages/valory/skills/reset_pause_abci/skill.yaml new file mode 100644 index 0000000..e0b97cf --- /dev/null +++ b/packages/valory/skills/reset_pause_abci/skill.yaml @@ -0,0 +1,141 @@ +name: reset_pause_abci +author: valory +version: 0.1.0 +type: skill +description: ABCI application for resetting and pausing app executions. +license: Apache-2.0 +aea_version: '>=1.0.0, <2.0.0' +fingerprint: + README.md: bafybeigyx3zutnbq2sqlgeo2hi2vjgpmnlspnkyh4wemjfrqkrpel27bwi + __init__.py: bafybeicx55fcmu5t2lrrs4wqi6bdvsmoq2csfqebyzwy6oh4olmhnvmelu + behaviours.py: bafybeich7tmipn2zsuqsmhtbrmmqys3mpvn3jctx6g3kz2atet2atl3j6q + dialogues.py: bafybeigabhaykiyzbluu4mk6bbrmqhzld2kyp32pg24bvjmzrrb74einwm + fsm_specification.yaml: bafybeietrxvm2odv3si3ecep3by6rftsirzzazxpmeh73yvtsis2mfaali + handlers.py: bafybeie22h45jr2opf2waszr3qt5km2fppcaahalcavhzutgb6pyyywqxq + models.py: bafybeiagj2e73wvzfqti6chbgkxh5tawzdjwqnxlo2bcfa5lyzy6ogzh2u + payloads.py: bafybeihychpsosovpyq7bh6aih2cyjkxr23j7becd5apetrqivvnolzm7i + rounds.py: bafybeifi2gpj2piilxtqcvv6lxhwpnbl7xs3a3trh3wvlv2wihowoon4tm + tests/__init__.py: bafybeiclijinxvycj7agcagt2deuuyh7zxyp7k2s55la6lh3jghzqvfux4 + tests/test_behaviours.py: bafybeigblrmkjci6at74yetaevgw5zszhivpednbok7fby4tqn7zt2vemy + tests/test_dialogues.py: bafybeif7pe7v34cfznzv4htyuevx733ersmk4bqjcgajn2535jmuujdmzm + tests/test_handlers.py: bafybeiggog2k65ijtvqwkvjvmaoo6khwgfkeodddzl6u76gcvvongwjawy + tests/test_payloads.py: bafybeifj343tlaiasebfgahfxehn4oi74omgah3ju2pze2fefoouid2zdq + tests/test_rounds.py: bafybeifz67lfay4pkz5ipblpfpadl4zmd5riajkv6sdsiby22z24gp3cxa +fingerprint_ignore_patterns: [] +connections: [] +contracts: [] +protocols: [] +skills: +- valory/abstract_round_abci:0.1.0:bafybeibovsktd3uxur45nrcomq5shcn46cgxd5idmhxbmjhg32c5abyqim +behaviours: + main: + args: {} + class_name: ResetPauseABCIConsensusBehaviour +handlers: + abci: + args: {} + class_name: ABCIHandler + contract_api: + args: {} + class_name: ContractApiHandler + http: + args: {} + class_name: HttpHandler + ipfs: + args: {} + class_name: IpfsHandler + ledger_api: + args: {} + class_name: LedgerApiHandler + signing: + args: {} + class_name: SigningHandler + tendermint: + args: {} + class_name: TendermintHandler +models: + abci_dialogues: + args: {} + class_name: AbciDialogues + benchmark_tool: + args: + log_dir: /logs + class_name: BenchmarkTool + contract_api_dialogues: + args: {} + class_name: ContractApiDialogues + http_dialogues: + args: {} + class_name: HttpDialogues + ipfs_dialogues: + args: {} + class_name: IpfsDialogues + ledger_api_dialogues: + args: {} + class_name: LedgerApiDialogues + params: + args: + cleanup_history_depth: 1 + cleanup_history_depth_current: null + drand_public_key: 868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31 + genesis_config: + genesis_time: '2022-05-20T16:00:21.735122717Z' + chain_id: chain-c4daS1 + consensus_params: + block: + max_bytes: '22020096' + max_gas: '-1' + time_iota_ms: '1000' + evidence: + max_age_num_blocks: '100000' + max_age_duration: '172800000000000' + max_bytes: '1048576' + validator: + pub_key_types: + - ed25519 + version: {} + voting_power: '10' + keeper_timeout: 30.0 + light_slash_unit_amount: 5000000000000000 + max_attempts: 10 + max_healthcheck: 120 + on_chain_service_id: null + request_retry_delay: 1.0 + request_timeout: 10.0 + reset_pause_duration: 10 + reset_tendermint_after: 2 + retry_attempts: 400 + retry_timeout: 3 + round_timeout_seconds: 30.0 + serious_slash_unit_amount: 8000000000000000 + service_id: reset_pause_abci + service_registry_address: null + setup: {} + share_tm_config_on_startup: false + slash_cooldown_hours: 3 + slash_threshold_amount: 10000000000000000 + sleep_time: 1 + tendermint_check_sleep_delay: 3 + tendermint_com_url: http://localhost:8080 + tendermint_max_retries: 5 + tendermint_p2p_url: localhost:26656 + tendermint_url: http://localhost:26657 + tx_timeout: 10.0 + use_slashing: false + use_termination: false + class_name: Params + requests: + args: {} + class_name: Requests + signing_dialogues: + args: {} + class_name: SigningDialogues + state: + args: {} + class_name: SharedState + tendermint_dialogues: + args: {} + class_name: TendermintDialogues +dependencies: {} +is_abstract: true +customs: [] diff --git a/packages/valory/skills/reset_pause_abci/tests/__init__.py b/packages/valory/skills/reset_pause_abci/tests/__init__.py new file mode 100644 index 0000000..db0918f --- /dev/null +++ b/packages/valory/skills/reset_pause_abci/tests/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Tests for valory/reset_pause_abci skill.""" diff --git a/packages/valory/skills/reset_pause_abci/tests/test_behaviours.py b/packages/valory/skills/reset_pause_abci/tests/test_behaviours.py new file mode 100644 index 0000000..5435676 --- /dev/null +++ b/packages/valory/skills/reset_pause_abci/tests/test_behaviours.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Tests for valory/reset_pause_abci skill's behaviours.""" + +# pylint: skip-file + +from pathlib import Path +from typing import Callable, Generator, Optional +from unittest import mock +from unittest.mock import MagicMock + +import pytest + +from packages.valory.skills.abstract_round_abci.base import AbciAppDB +from packages.valory.skills.abstract_round_abci.base import ( + BaseSynchronizedData as ResetSynchronizedSata, +) +from packages.valory.skills.abstract_round_abci.behaviour_utils import ( + make_degenerate_behaviour, +) +from packages.valory.skills.abstract_round_abci.test_tools.base import ( + FSMBehaviourBaseCase, +) +from packages.valory.skills.reset_pause_abci import PUBLIC_ID +from packages.valory.skills.reset_pause_abci.behaviours import ResetAndPauseBehaviour +from packages.valory.skills.reset_pause_abci.rounds import Event as ResetEvent +from packages.valory.skills.reset_pause_abci.rounds import FinishedResetAndPauseRound + + +PACKAGE_DIR = Path(__file__).parent.parent + + +def test_skill_public_id() -> None: + """Test skill module public ID""" + + assert PUBLIC_ID.name == Path(__file__).parents[1].name + assert PUBLIC_ID.author == Path(__file__).parents[3].name + + +class ResetPauseAbciFSMBehaviourBaseCase(FSMBehaviourBaseCase): + """Base case for testing PauseReset FSMBehaviour.""" + + path_to_skill = PACKAGE_DIR + + +def dummy_reset_tendermint_with_wait_wrapper( + reset_successfully: Optional[bool], +) -> Callable[[], Generator[None, None, Optional[bool]]]: + """Wrapper for a Dummy `reset_tendermint_with_wait` method.""" + + def dummy_reset_tendermint_with_wait() -> Generator[None, None, Optional[bool]]: + """Dummy `reset_tendermint_with_wait` method.""" + yield + return reset_successfully + + return dummy_reset_tendermint_with_wait + + +class TestResetAndPauseBehaviour(ResetPauseAbciFSMBehaviourBaseCase): + """Test ResetBehaviour.""" + + behaviour_class = ResetAndPauseBehaviour + next_behaviour_class = make_degenerate_behaviour(FinishedResetAndPauseRound) + + @pytest.mark.parametrize("tendermint_reset_status", (None, True, False)) + def test_reset_behaviour( + self, + tendermint_reset_status: Optional[bool], + ) -> None: + """Test reset behaviour.""" + dummy_participants = [[i for i in range(4)]] + + self.fast_forward_to_behaviour( + behaviour=self.behaviour, + behaviour_id=self.behaviour_class.auto_behaviour_id(), + synchronized_data=ResetSynchronizedSata( + AbciAppDB( + setup_data=dict( + all_participants=dummy_participants, + participants=dummy_participants, + safe_contract_address=[""], + consensus_threshold=[3], + most_voted_estimate=[0.1], + tx_hashes_history=[["68656c6c6f776f726c64"]], + ), + ) + ), + ) + + assert self.behaviour.current_behaviour is not None + assert ( + self.behaviour.current_behaviour.behaviour_id + == self.behaviour_class.auto_behaviour_id() + ) + + with mock.patch.object( + self.behaviour.current_behaviour, + "send_a2a_transaction", + side_effect=self.behaviour.current_behaviour.send_a2a_transaction, + ), mock.patch.object( + self.behaviour.current_behaviour, + "reset_tendermint_with_wait", + side_effect=dummy_reset_tendermint_with_wait_wrapper( + tendermint_reset_status + ), + ) as mock_reset_tendermint_with_wait, mock.patch.object( + self.behaviour.current_behaviour, + "wait_from_last_timestamp", + side_effect=lambda _: (yield), + ): + if tendermint_reset_status is not None: + # Increase the period_count to force the call to reset_tendermint_with_wait() + self.behaviour.current_behaviour.synchronized_data.create() + + self.behaviour.act_wrapper() + self.behaviour.act_wrapper() + + # now if the first reset attempt has been simulated to fail, let's simulate the second attempt to succeed. + if tendermint_reset_status is not None and not tendermint_reset_status: + mock_reset_tendermint_with_wait.side_effect = ( + dummy_reset_tendermint_with_wait_wrapper(True) + ) + self.behaviour.act_wrapper() + # make sure that the behaviour does not send any txs to other agents when Tendermint reset fails + assert isinstance( + self.behaviour.current_behaviour.send_a2a_transaction, MagicMock + ) + self.behaviour.current_behaviour.send_a2a_transaction.assert_not_called() + self.behaviour.act_wrapper() + + self.mock_a2a_transaction() + self._test_done_flag_set() + self.end_round(ResetEvent.DONE) + assert ( + self.behaviour.current_behaviour.behaviour_id + == self.next_behaviour_class.auto_behaviour_id() + ) diff --git a/packages/valory/skills/reset_pause_abci/tests/test_dialogues.py b/packages/valory/skills/reset_pause_abci/tests/test_dialogues.py new file mode 100644 index 0000000..d9b2904 --- /dev/null +++ b/packages/valory/skills/reset_pause_abci/tests/test_dialogues.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test the dialogues.py module of the skill.""" + +# pylint: skip-file + +import packages.valory.skills.reset_pause_abci.dialogues # noqa + + +def test_import() -> None: + """Test that the 'dialogues.py' Python module can be imported.""" diff --git a/packages/valory/skills/reset_pause_abci/tests/test_handlers.py b/packages/valory/skills/reset_pause_abci/tests/test_handlers.py new file mode 100644 index 0000000..347f9e9 --- /dev/null +++ b/packages/valory/skills/reset_pause_abci/tests/test_handlers.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test the dialogues.py module of the skill.""" + +# pylint: skip-file + +import packages.valory.skills.reset_pause_abci.handlers # noqa + + +def test_import() -> None: + """Test that the 'handlers.py' Python module can be imported.""" diff --git a/packages/valory/skills/reset_pause_abci/tests/test_payloads.py b/packages/valory/skills/reset_pause_abci/tests/test_payloads.py new file mode 100644 index 0000000..15b67fa --- /dev/null +++ b/packages/valory/skills/reset_pause_abci/tests/test_payloads.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test the payloads.py module of the skill.""" + +# pylint: skip-file + +from packages.valory.skills.reset_pause_abci.payloads import ResetPausePayload + + +def test_reset_pause_payload() -> None: + """Test `ResetPausePayload`.""" + + payload = ResetPausePayload(sender="sender", period_count=1) + + assert payload.period_count == 1 + assert payload.data == {"period_count": 1} + assert ResetPausePayload.from_json(payload.json) == payload diff --git a/packages/valory/skills/reset_pause_abci/tests/test_rounds.py b/packages/valory/skills/reset_pause_abci/tests/test_rounds.py new file mode 100644 index 0000000..adadb77 --- /dev/null +++ b/packages/valory/skills/reset_pause_abci/tests/test_rounds.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test the rounds of the skill.""" + +# pylint: skip-file + +import hashlib +import logging # noqa: F401 +from typing import Dict, FrozenSet +from unittest.mock import MagicMock + +from packages.valory.skills.abstract_round_abci.base import ( + BaseSynchronizedData as ResetSynchronizedSata, +) +from packages.valory.skills.abstract_round_abci.test_tools.rounds import ( + BaseCollectSameUntilThresholdRoundTest, +) +from packages.valory.skills.reset_pause_abci.payloads import ResetPausePayload +from packages.valory.skills.reset_pause_abci.rounds import Event as ResetEvent +from packages.valory.skills.reset_pause_abci.rounds import ResetAndPauseRound + + +MAX_PARTICIPANTS: int = 4 +DUMMY_RANDOMNESS = hashlib.sha256("hash".encode() + str(0).encode()).hexdigest() + + +def get_participant_to_period_count( + participants: FrozenSet[str], period_count: int +) -> Dict[str, ResetPausePayload]: + """participant_to_selection""" + return { + participant: ResetPausePayload(sender=participant, period_count=period_count) + for participant in participants + } + + +class TestResetAndPauseRound(BaseCollectSameUntilThresholdRoundTest): + """Test ResetRound.""" + + _synchronized_data_class = ResetSynchronizedSata + _event_class = ResetEvent + + def test_runs( + self, + ) -> None: + """Runs tests.""" + + synchronized_data = self.synchronized_data.update( + most_voted_randomness=DUMMY_RANDOMNESS, consensus_threshold=3 + ) + synchronized_data._db._cross_period_persisted_keys = frozenset( + {"most_voted_randomness"} + ) + test_round = ResetAndPauseRound( + synchronized_data=synchronized_data, + context=MagicMock(), + ) + next_period_count = 1 + self._complete_run( + self._test_round( + test_round=test_round, + round_payloads=get_participant_to_period_count( + self.participants, next_period_count + ), + synchronized_data_update_fn=lambda _synchronized_data, _: _synchronized_data.create(), + synchronized_data_attr_checks=[], # [lambda _synchronized_data: _synchronized_data.participants], + most_voted_payload=next_period_count, + exit_event=self._event_class.DONE, + ) + ) + + def test_accepting_payloads_from(self) -> None: + """Test accepting payloads from""" + + alice, *others = self.participants + participants = list(others) + all_participants = participants + [alice] + + synchronized_data = self.synchronized_data.update( + participants=participants, all_participants=all_participants + ) + + test_round = ResetAndPauseRound( + synchronized_data=synchronized_data, + context=MagicMock(), + ) + + assert test_round.accepting_payloads_from != participants + assert test_round.accepting_payloads_from == frozenset(all_participants) diff --git a/packages/valory/skills/strategy_evaluator_abci/README.md b/packages/valory/skills/strategy_evaluator_abci/README.md new file mode 100644 index 0000000..5b6617a --- /dev/null +++ b/packages/valory/skills/strategy_evaluator_abci/README.md @@ -0,0 +1,6 @@ +# StrategyEvaluator abci + +## Description + +This module contains an ABCI skill responsible for the execution of a trading strategy, +and the preparation of a swapping transaction for a Solana trading AEA. diff --git a/packages/valory/skills/strategy_evaluator_abci/__init__.py b/packages/valory/skills/strategy_evaluator_abci/__init__.py new file mode 100644 index 0000000..f399306 --- /dev/null +++ b/packages/valory/skills/strategy_evaluator_abci/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the strategy evaluator skill for the trader.""" + +from aea.configurations.base import PublicId + + +PUBLIC_ID = PublicId.from_str("valory/strategy_evaluator_abci:0.1.0") diff --git a/packages/valory/skills/strategy_evaluator_abci/behaviours/__init__.py b/packages/valory/skills/strategy_evaluator_abci/behaviours/__init__.py new file mode 100644 index 0000000..b05652b --- /dev/null +++ b/packages/valory/skills/strategy_evaluator_abci/behaviours/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains the behaviours for the 'strategy_evaluator_abci' skill.""" diff --git a/packages/valory/skills/strategy_evaluator_abci/behaviours/backtesting.py b/packages/valory/skills/strategy_evaluator_abci/behaviours/backtesting.py new file mode 100644 index 0000000..f9cae76 --- /dev/null +++ b/packages/valory/skills/strategy_evaluator_abci/behaviours/backtesting.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the behaviour for backtesting the swap(s).""" + +from typing import Any, Dict, Generator, List, Optional, Tuple, cast + +from packages.valory.skills.abstract_round_abci.io_.store import SupportedFiletype +from packages.valory.skills.strategy_evaluator_abci.behaviours.base import ( + CALLABLE_KEY, + STRATEGY_KEY, + StrategyEvaluatorBaseBehaviour, +) +from packages.valory.skills.strategy_evaluator_abci.behaviours.strategy_exec import ( + OUTPUT_MINT, + TRANSFORMED_PRICE_DATA_KEY, +) +from packages.valory.skills.strategy_evaluator_abci.states.backtesting import ( + BacktestRound, +) + + +EVALUATE_CALLABLE_KEY = "evaluate_callable" +ASSET_KEY = "asset" +BACKTEST_RESULT_KEY = "sharpe_ratio" + + +class BacktestBehaviour(StrategyEvaluatorBaseBehaviour): + """A behaviour in which the agents backtest the swap(s).""" + + matching_round = BacktestRound + + def backtest(self, transformed_data: Dict[str, Any], output_mint: str) -> bool: + """Backtest the given token and return whether the sharpe ratio is greater than the threshold.""" + token_data = transformed_data.get(output_mint, None) + if token_data is None: + self.context.logger.error( + f"No data were found in the fetched transformed data for token {output_mint!r}." + ) + return False + + # the following are always passed to a strategy script, which may choose to ignore any + kwargs: Dict[str, Any] = self.params.strategies_kwargs + kwargs.update( + { + STRATEGY_KEY: self.synchronized_data.selected_strategy, + CALLABLE_KEY: EVALUATE_CALLABLE_KEY, + # TODO it is not clear which asset's data we should pass here + # shouldn't the evaluate method take both input and output token's data into account? + TRANSFORMED_PRICE_DATA_KEY: token_data, + ASSET_KEY: output_mint, + } + ) + results = self.execute_strategy_callable(**kwargs) + if results is None: + self.context.logger.error( + f"Something went wrong while backtesting token {output_mint!r}." + ) + return False + self.log_from_strategy_results(results) + sharpe: Optional[float] = results.get(BACKTEST_RESULT_KEY, None) + if sharpe is None or not isinstance(sharpe, float): + self.context.logger.error( + f"No float sharpe value can be extracted using key {BACKTEST_RESULT_KEY!r} in strategy's {results=}." + ) + return False + + self.context.logger.info(f"{sharpe=}.") + return sharpe >= self.params.sharpe_threshold + + def filter_orders( + self, orders: List[Dict[str, str]] + ) -> Generator[None, None, Tuple[List[Dict[str, str]], bool]]: + """Backtest the swap(s) and decide whether we should proceed to perform them or not.""" + transformed_data = yield from self.get_from_ipfs( + self.synchronized_data.transformed_data_hash, SupportedFiletype.JSON + ) + transformed_data = cast(Optional[Dict[str, Any]], transformed_data) + if transformed_data is None: + self.context.logger.error("Could not get the transformed data from IPFS.") + # return empty orders and incomplete status, because the transformed data are necessary for the backtesting + return [], True + + self.context.logger.info( + f"Using trading strategy {self.synchronized_data.selected_strategy!r} for backtesting..." + ) + + success_orders = [] + incomplete = False + for order in orders: + token = order.get(OUTPUT_MINT, None) + if token is None: + err = f"{OUTPUT_MINT!r} key was not found in {order=}." + self.context.logger.error(err) + incomplete = True + continue + + backtest_passed = self.backtest(transformed_data, token) + if backtest_passed: + success_orders.append(order) + continue + + incomplete = True + + if len(success_orders) == 0: + incomplete = True + + return success_orders, incomplete + + def async_act(self) -> Generator: + """Do the action.""" + yield from self.get_process_store_act( + self.synchronized_data.orders_hash, + self.filter_orders, + str(self.swap_decision_filepath), + ) diff --git a/packages/valory/skills/strategy_evaluator_abci/behaviours/base.py b/packages/valory/skills/strategy_evaluator_abci/behaviours/base.py new file mode 100644 index 0000000..3a599d4 --- /dev/null +++ b/packages/valory/skills/strategy_evaluator_abci/behaviours/base.py @@ -0,0 +1,293 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the base behaviour for the 'strategy_evaluator_abci' skill.""" + +import json +from abc import ABC +from pathlib import Path +from typing import Any, Callable, Dict, Generator, Optional, Sized, Tuple, cast + +from packages.valory.skills.abstract_round_abci.base import BaseTxPayload +from packages.valory.skills.abstract_round_abci.behaviour_utils import BaseBehaviour +from packages.valory.skills.abstract_round_abci.io_.load import CustomLoaderType +from packages.valory.skills.abstract_round_abci.io_.store import ( + SupportedFiletype, + SupportedObjectType, +) +from packages.valory.skills.abstract_round_abci.models import ApiSpecs +from packages.valory.skills.strategy_evaluator_abci.models import ( + SharedState, + StrategyEvaluatorParams, +) +from packages.valory.skills.strategy_evaluator_abci.payloads import IPFSHashPayload +from packages.valory.skills.strategy_evaluator_abci.states.base import SynchronizedData + + +SWAP_DECISION_FILENAME = "swap_decision.json" +SWAP_INSTRUCTIONS_FILENAME = "swap_instructions.json" +STRATEGY_KEY = "trading_strategy" +CALLABLE_KEY = "callable" +ENTRY_POINT_STORE_KEY = "entry_point" +SUPPORTED_STRATEGY_LOG_LEVELS = ("info", "warning", "error") + + +def wei_to_native(wei: int) -> float: + """Convert WEI to native token.""" + return wei / 10**18 + + +def to_content(content: dict) -> bytes: + """Convert the given content to bytes' payload.""" + return json.dumps(content, sort_keys=True).encode() + + +class StrategyEvaluatorBaseBehaviour(BaseBehaviour, ABC): + """Represents the base class for the strategy evaluation FSM behaviour.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize the strategy evaluator behaviour.""" + super().__init__(**kwargs) + self.swap_decision_filepath = ( + Path(self.context.data_dir) / SWAP_DECISION_FILENAME + ) + self.swap_instructions_filepath = ( + Path(self.context.data_dir) / SWAP_INSTRUCTIONS_FILENAME + ) + self.token_balance = 0 + self.wallet_balance = 0 + + @property + def params(self) -> StrategyEvaluatorParams: + """Return the params.""" + return cast(StrategyEvaluatorParams, self.context.params) + + @property + def shared_state(self) -> SharedState: + """Get the shared state.""" + return cast(SharedState, self.context.state) + + @property + def synchronized_data(self) -> SynchronizedData: + """Return the synchronized data.""" + return SynchronizedData(super().synchronized_data.db) + + def strategy_store(self, strategy_name: str) -> Dict[str, str]: + """Get the stored strategy's files.""" + return self.context.shared_state.get(strategy_name, {}) + + def execute_strategy_callable( + self, *args: Any, **kwargs: Any + ) -> Dict[str, Any] | None: + """Execute a strategy's method and return the results.""" + trading_strategy: Optional[str] = kwargs.pop(STRATEGY_KEY, None) + if trading_strategy is None: + self.context.logger.error(f"No {STRATEGY_KEY!r} was given!") + return None + + callable_key: Optional[str] = kwargs.pop(CALLABLE_KEY, None) + if callable_key is None: + self.context.logger.error(f"No {CALLABLE_KEY!r} was given!") + return None + + store = self.strategy_store(trading_strategy) + strategy_exec = store.get(ENTRY_POINT_STORE_KEY, None) + if strategy_exec is None: + self.context.logger.error( + f"No executable was found for {trading_strategy=}! Did the IPFS package downloader load it correctly?" + ) + return None + + callable_method = store.get(callable_key, None) + if callable_method is None: + self.context.logger.error( + f"No {callable_method=} was found in the loaded component! " + "Did the IPFS package downloader load it correctly?" + ) + return None + + if callable_method in globals(): + del globals()[callable_method] + + exec(strategy_exec, globals()) # pylint: disable=W0122 # nosec + method: Optional[Callable] = globals().get(callable_method, None) + if method is None: + self.context.logger.error( + f"No {callable_method!r} method was found in {trading_strategy} strategy's executable:\n" + f"{strategy_exec}." + ) + return None + # TODO this method is blocking, needs to be run from an aea skill or a task. + return method(*args, **kwargs) + + def log_from_strategy_results(self, results: Dict[str, Any]) -> None: + """Log any messages from a strategy's results.""" + for level in SUPPORTED_STRATEGY_LOG_LEVELS: + logger = getattr(self.context.logger, level, None) + if logger is not None: + for log in results.get(level, []): + logger(log) + + def _handle_response( + self, + api: ApiSpecs, + res: Optional[dict], + ) -> Generator[None, None, Optional[Any]]: + """Handle the response from an API. + + :param api: the `ApiSpecs` instance of the API. + :param res: the response to handle. + :return: the response's result, using the given keys. `None` if response is `None` (has failed). + :yield: None + """ + if res is None: + error = f"Could not get a response from {api.api_id!r} API." + self.context.logger.error(error) + api.increment_retries() + yield from self.sleep(api.retries_info.suggested_sleep_time) + return None + + self.context.logger.info( + f"Retrieved a response from {api.api_id!r} API: {res}." + ) + api.reset_retries() + return res + + def _get_response( + self, + api: ApiSpecs, + dynamic_parameters: Dict[str, str], + content: Optional[dict] = None, + ) -> Generator[None, None, Any]: + """Get the response from an API.""" + specs = api.get_spec() + specs["parameters"].update(dynamic_parameters) + if content is not None: + specs["content"] = to_content(content) + + while not api.is_retries_exceeded(): + res_raw = yield from self.get_http_response(**specs) + res = api.process_response(res_raw) + response = yield from self._handle_response(api, res) + if response is not None: + return response + + error = f"Retries were exceeded for {api.api_id!r} API." + self.context.logger.error(error) + api.reset_retries() + return None + + def get_from_ipfs( + self, + ipfs_hash: Optional[str], + filetype: Optional[SupportedFiletype] = None, + custom_loader: CustomLoaderType = None, + timeout: Optional[float] = None, + ) -> Generator[None, None, Optional[SupportedObjectType]]: + """ + Gets an object from IPFS. + + If the result is `None`, then an error is logged, sleeps, and retries. + + :param ipfs_hash: the ipfs hash of the file/dir to download. + :param filetype: the file type of the object being downloaded. + :param custom_loader: a custom deserializer for the object received from IPFS. + :param timeout: timeout for the request. + :yields: None. + :returns: the downloaded object, corresponding to ipfs_hash or `None` if retries were exceeded. + """ + if ipfs_hash is None: + return None + + n_retries = 0 + while n_retries < self.params.ipfs_fetch_retries: + res = yield from super().get_from_ipfs( + ipfs_hash, filetype, custom_loader, timeout + ) + if res is not None: + return res + + n_retries += 1 + sleep_time = self.params.sleep_time + self.context.logger.error( + f"Could not get any data from IPFS using hash {ipfs_hash!r}!" + f"Retrying in {sleep_time}..." + ) + yield from self.sleep(sleep_time) + + return None + + def get_ipfs_hash_payload_content( + self, + data: Any, + process_fn: Callable[[Any], Generator[None, None, Tuple[Sized, bool]]], + store_filepath: str, + ) -> Generator[None, None, Tuple[Optional[str], Optional[bool]]]: + """Get the ipfs hash payload's content.""" + if data is None: + return None, None + + incomplete: Optional[bool] + processed, incomplete = yield from process_fn(data) + if len(processed) == 0: + processed_hash = None + if incomplete: + incomplete = None + else: + processed_hash = yield from self.send_to_ipfs( + store_filepath, + processed, + filetype=SupportedFiletype.JSON, + ) + return processed_hash, incomplete + + def get_process_store_act( + self, + hash_: Optional[str], + process_fn: Callable[[Any], Generator[None, None, Tuple[Sized, bool]]], + store_filepath: str, + ) -> Generator: + """An async act method for getting some data, processing them, and storing the result. + + 1. Get some data using the given hash. + 2. Process them using the given fn. + 3. Send them to IPFS using the given filepath as intermediate storage. + + :param hash_: the hash of the data to process. + :param process_fn: the function to process the data. + :param store_filepath: path to the file to store the processed data. + :yield: None + """ + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + data = yield from self.get_from_ipfs(hash_, SupportedFiletype.JSON) + sender = self.context.agent_address + payload_data = yield from self.get_ipfs_hash_payload_content( + data, process_fn, store_filepath + ) + payload = IPFSHashPayload(sender, *payload_data) + + yield from self.finish_behaviour(payload) + + def finish_behaviour(self, payload: BaseTxPayload) -> Generator: + """Finish the behaviour.""" + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + yield from self.send_a2a_transaction(payload) + yield from self.wait_until_round_end() + + self.set_done() diff --git a/packages/valory/skills/strategy_evaluator_abci/behaviours/prepare_swap_tx.py b/packages/valory/skills/strategy_evaluator_abci/behaviours/prepare_swap_tx.py new file mode 100644 index 0000000..b83a7f4 --- /dev/null +++ b/packages/valory/skills/strategy_evaluator_abci/behaviours/prepare_swap_tx.py @@ -0,0 +1,412 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the behaviour for preparing swap(s) instructions.""" + +import json +import traceback +from typing import Any, Callable, Dict, Generator, List, Optional, Sized, Tuple, cast + +from packages.eightballer.connections.dcxt import PUBLIC_ID as DCXT_ID +from packages.eightballer.protocols.orders.custom_types import ( + Order, + OrderSide, + OrderType, +) +from packages.eightballer.protocols.orders.message import OrdersMessage +from packages.valory.contracts.gnosis_safe.contract import GnosisSafeContract +from packages.valory.protocols.contract_api.message import ContractApiMessage +from packages.valory.skills.abstract_round_abci.base import ( + BaseTxPayload, + LEDGER_API_ADDRESS, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + ContractApiDialogue, + ContractApiDialogues, +) +from packages.valory.skills.abstract_round_abci.io_.store import SupportedFiletype +from packages.valory.skills.abstract_round_abci.models import Requests +from packages.valory.skills.strategy_evaluator_abci.behaviours.base import ( + StrategyEvaluatorBaseBehaviour, +) +from packages.valory.skills.strategy_evaluator_abci.payloads import ( + TransactionHashPayload, +) +from packages.valory.skills.strategy_evaluator_abci.states.prepare_swap import ( + PrepareEvmSwapRound, + PrepareSwapRound, +) +from packages.valory.skills.transaction_settlement_abci.payload_tools import ( + hash_payload_to_hex, +) +from packages.valory.skills.transaction_settlement_abci.rounds import TX_HASH_LENGTH + + +SAFE_GAS = 0 + + +class PrepareSwapBehaviour(StrategyEvaluatorBaseBehaviour): + """A behaviour in which the agents execute the selected strategy and decide on the swap(s).""" + + matching_round = PrepareSwapRound + + def __init__(self, **kwargs: Any): + """Initialize the swap-preparation behaviour.""" + super().__init__(**kwargs) + self.incomplete = False + + def setup(self) -> None: + """Initialize the behaviour.""" + self.context.swap_quotes.reset_retries() + self.context.swap_instructions.reset_retries() + + def build_quote( + self, quote_data: Dict[str, str] + ) -> Generator[None, None, Optional[dict]]: + """Build the quote.""" + response = yield from self._get_response(self.context.swap_quotes, quote_data) + return response + + def build_instructions(self, quote: dict) -> Generator[None, None, Optional[dict]]: + """Build the instructions.""" + content = { + "quoteResponse": quote, + "userPublicKey": self.context.agent_address, + } + response = yield from self._get_response( + self.context.swap_instructions, + dynamic_parameters={}, + content=content, + ) + return response + + def build_swap_tx( + self, quote_data: Dict[str, str] + ) -> Generator[None, None, Optional[Dict[str, Any]]]: + """Build instructions for a swap transaction.""" + quote = yield from self.build_quote(quote_data) + if quote is None: + return None + instructions = yield from self.build_instructions(quote) + return instructions + + def prepare_instructions( + self, orders: List[Dict[str, str]] + ) -> Generator[None, None, Tuple[List[Dict[str, Any]], bool]]: + """Prepare the instructions for a Swap transaction.""" + instructions = [] + for quote_data in orders: + swap_instruction = yield from self.build_swap_tx(quote_data) + if swap_instruction is None: + self.incomplete = True + else: + instructions.append(swap_instruction) + + return instructions, self.incomplete + + def async_act(self) -> Generator: + """Do the action.""" + yield from self.get_process_store_act( + self.synchronized_data.backtested_orders_hash, + self.prepare_instructions, + str(self.swap_instructions_filepath), + ) + + +class PrepareEvmSwapBehaviour(StrategyEvaluatorBaseBehaviour): + """A behaviour in which the agents execute the selected strategy and decide on the swap(s).""" + + matching_round = PrepareEvmSwapRound + + def __init__(self, **kwargs: Any): + """Initialize the swap-preparation behaviour.""" + super().__init__(**kwargs) + self.incomplete = False + self._performative_to_dialogue_class = { + OrdersMessage.Performative.CREATE_ORDER: self.context.orders_dialogues, + } + + def setup(self) -> None: + """Initialize the behaviour.""" + self.context.swap_quotes.reset_retries() + self.context.swap_instructions.reset_retries() + + def build_quote( + self, quote_data: Dict[str, str] + ) -> Generator[None, None, Optional[dict]]: + """Build the quote.""" + response = yield from self._get_response(self.context.swap_quotes, quote_data) + return response + + def build_instructions(self, quote: dict) -> Generator[None, None, Optional[dict]]: + """Build the instructions.""" + content = { + "quoteResponse": quote, + "userPublicKey": self.context.agent_address, + } + response = yield from self._get_response( + self.context.swap_instructions, + dynamic_parameters={}, + content=content, + ) + return response + + def build_swap_tx( + self, quote_data: Dict[str, str] + ) -> Generator[None, None, Optional[Dict[str, Any]]]: + """Build instructions for a swap transaction.""" + quote = yield from self.build_quote(quote_data) + if quote is None: + return None + instructions = yield from self.build_instructions(quote) + return instructions + + def prepare_transactions( + self, orders: List[Dict[str, str]] + ) -> Generator[None, None, Tuple[List[Dict[str, Any]], bool]]: + """Prepare the instructions for a Swap transaction.""" + instructions = [] + for quote_data in orders: + symbol = f'{quote_data["inputMint"]}/{quote_data["outputMint"]}' + # We assume for now that we are only sending to the one exchange + ledger_id: str = self.params.ledger_ids[0] + exchange_ids = self.params.exchange_ids[ledger_id] + if len(exchange_ids) != 1: + self.context.logger.error( + f"Expected exactly one exchange id, got {exchange_ids}." + ) + raise ValueError( + f"Expected exactly one exchange id, got {exchange_ids}." + ) + exchange_id = f"{exchange_ids[0]}_{ledger_id}" + + order = Order( + exchange_id=exchange_id, + symbol=symbol, + amount=self.params.trade_size_in_base_token, + side=OrderSide.BUY, + type=OrderType.MARKET, + data=json.dumps( + { + "safe_contract_address": self.synchronized_data.safe_contract_address, + } + ), + ) + + result = yield from self.get_dcxt_response( + protocol_performative=OrdersMessage.Performative.CREATE_ORDER, # type: ignore + order=order, + ) + call_data = result.order.data + try: + can_create_hash = yield from self._build_safe_tx_hash( + vault_address=call_data["vault_address"], + chain_id=call_data["chain_id"], + call_data=bytes.fromhex(call_data["data"][2:]), + ) + except Exception as e: + can_create_hash = False + self.context.logger.error( + f"Error building safe tx hash: {traceback.format_exc()} with error {e}" + ) + + if call_data is None: + self.incomplete = not can_create_hash + else: + instructions.append(call_data) + + return instructions, self.incomplete + + def _build_safe_tx_hash( + self, + vault_address: str, + chain_id: int, + call_data: bytes, + ) -> Any: + """Prepares and returns the safe tx hash for a multisend tx.""" + self.context.logger.info( + f"Building safe tx hash: safe={self.synchronized_data.safe_contract_address}\n" + + f"vault={vault_address}\n" + + f"chain_id={chain_id}\n" + + f"call_data={call_data.hex()}" + ) + + ledger_id: str = self.params.ledger_ids[0] + response_msg = yield from self.get_contract_api_response( + performative=ContractApiMessage.Performative.GET_RAW_TRANSACTION, # type: ignore + contract_address=self.synchronized_data.safe_contract_address, + contract_id=str(GnosisSafeContract.contract_id), + contract_callable="get_raw_safe_transaction_hash", + to_address=vault_address, + value=0, + data=call_data, + safe_tx_gas=SAFE_GAS, + ledger_id="ethereum", + chain_id=ledger_id, + ) + + if response_msg.performative != ContractApiMessage.Performative.RAW_TRANSACTION: + self.context.logger.error( + "Couldn't get safe tx hash. Expected response performative " + f"{ContractApiMessage.Performative.RAW_TRANSACTION.value}, " # type: ignore + f"received {response_msg.performative.value}: {response_msg}." + ) + return False + + tx_hash = response_msg.raw_transaction.body.get("tx_hash", None) + if tx_hash is None or len(tx_hash) != TX_HASH_LENGTH: + self.context.logger.error( + "Something went wrong while trying to get the buy transaction's hash. " + f"Invalid hash {tx_hash!r} was returned." + ) + return False + + safe_tx_hash = tx_hash[2:] + self.context.logger.info(f"Hash of the Safe transaction: {safe_tx_hash}") + # temp hack: + payload_string = hash_payload_to_hex( + safe_tx_hash, 0, SAFE_GAS, vault_address, call_data + ) + self.safe_tx_hash = safe_tx_hash + self.payload_string = payload_string + self.call_data = call_data + return True + + def async_act(self) -> Generator: + """Do the action.""" + yield from self.get_process_store_act( + self.synchronized_data.backtested_orders_hash, + self.prepare_transactions, + str(self.swap_instructions_filepath), + ) + + def get_dcxt_response( + self, + protocol_performative: OrdersMessage.Performative, + **kwargs: Any, + ) -> Generator[None, None, Any]: + """Get a ccxt response.""" + if protocol_performative not in self._performative_to_dialogue_class: + raise ValueError( + f"Unsupported protocol performative {protocol_performative}." + ) + dialogue_class = self._performative_to_dialogue_class[protocol_performative] + + msg, dialogue = dialogue_class.create( + counterparty=str(DCXT_ID), + performative=protocol_performative, + **kwargs, + ) + msg._sender = str(self.context.skill_id) # pylint: disable=protected-access + response = yield from self._do_request(msg, dialogue) + return response + + def get_process_store_act( + self, + hash_: Optional[str], + process_fn: Callable[[Any], Generator[None, None, Tuple[Sized, bool]]], + store_filepath: str, + ) -> Generator: + """An async act method for getting some data, processing them, and storing the result. + + 1. Get some data using the given hash. + 2. Process them using the given fn. + 3. Send them to IPFS using the given filepath as intermediate storage. + + :param hash_: the hash of the data to process. + :param process_fn: the function to process the data. + :param store_filepath: path to the file to store the processed data. + :yield: None + """ + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + data = yield from self.get_from_ipfs(hash_, SupportedFiletype.JSON) + sender = self.context.agent_address + yield from self.get_ipfs_hash_payload_content( + data, process_fn, store_filepath + ) + + payload = TransactionHashPayload( + sender, + tx_hash=self.payload_string, + ) + + yield from self.finish_behaviour(payload) + + def finish_behaviour(self, payload: BaseTxPayload) -> Generator: + """Finish the behaviour.""" + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + yield from self.send_a2a_transaction(payload) + yield from self.wait_until_round_end() + + self.set_done() + + def get_contract_api_response( + self, + performative: ContractApiMessage.Performative, + contract_address: Optional[str], + contract_id: str, + contract_callable: str, + ledger_id: Optional[str] = None, + **kwargs: Any, + ) -> Generator[None, None, ContractApiMessage]: + """ + Request contract safe transaction hash + + Happy-path full flow of the messages. + + AbstractRoundAbci skill -> (ContractApiMessage | ContractApiMessage.Performative) -> Ledger connection (contract dispatcher) + Ledger connection (contract dispatcher) -> (ContractApiMessage | ContractApiMessage.Performative) -> AbstractRoundAbci skill + + :param performative: the message performative + :param contract_address: the contract address + :param contract_id: the contract id + :param contract_callable: the callable to call on the contract + :param ledger_id: the ledger id, if not specified, the default ledger id is used + :param kwargs: keyword argument for the contract api request + :return: the contract api response + :yields: the contract api response + """ + contract_api_dialogues = cast( + ContractApiDialogues, self.context.contract_api_dialogues + ) + kwargs = { + "performative": performative, + "counterparty": LEDGER_API_ADDRESS, + "ledger_id": ledger_id or self.context.default_ledger_id, + "contract_id": contract_id, + "callable": contract_callable, + "kwargs": ContractApiMessage.Kwargs(kwargs), + } + if contract_address is not None: + kwargs["contract_address"] = contract_address + contract_api_msg, contract_api_dialogue = contract_api_dialogues.create( + **kwargs + ) + contract_api_dialogue = cast( + ContractApiDialogue, + contract_api_dialogue, + ) + contract_api_dialogue.terms = self._get_default_terms() + request_nonce = self._get_request_nonce_from_dialogue(contract_api_dialogue) + cast(Requests, self.context.requests).request_id_to_callback[ + request_nonce + ] = self.get_callback_request() + self.context.outbox.put_message(message=contract_api_msg) + response = yield from self.wait_for_message() + return response diff --git a/packages/valory/skills/strategy_evaluator_abci/behaviours/proxy_swap_queue.py b/packages/valory/skills/strategy_evaluator_abci/behaviours/proxy_swap_queue.py new file mode 100644 index 0000000..2efdc3a --- /dev/null +++ b/packages/valory/skills/strategy_evaluator_abci/behaviours/proxy_swap_queue.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the behaviour for sending a transaction for the next swap in the queue of orders.""" + +from typing import Dict, Generator, List, Optional, cast + +from packages.valory.skills.abstract_round_abci.io_.store import SupportedFiletype +from packages.valory.skills.strategy_evaluator_abci.behaviours.base import ( + StrategyEvaluatorBaseBehaviour, +) +from packages.valory.skills.strategy_evaluator_abci.models import TxSettlementProxy +from packages.valory.skills.strategy_evaluator_abci.payloads import SendSwapProxyPayload +from packages.valory.skills.strategy_evaluator_abci.states.proxy_swap_queue import ( + ProxySwapQueueRound, +) + + +OrdersType = Optional[List[Dict[str, str]]] + + +PROXY_STATUS_FIELD = "status" +PROXY_TX_ID_FIELD = "txId" +PROXY_TX_URL_FIELD = "url" +PROXY_ERROR_MESSAGE_FIELD = "message" +PROXY_SUCCESS_RESPONSE = "ok" +PROXY_ERROR_RESPONSE = "error" + + +class ProxySwapQueueBehaviour(StrategyEvaluatorBaseBehaviour): + """A behaviour in which the agent utilizes the proxy server to perform the next swap transaction in priority. + + Warning: This can only work with a single agent service. + """ + + matching_round = ProxySwapQueueRound + + def setup(self) -> None: + """Initialize the behaviour.""" + self.context.tx_settlement_proxy.reset_retries() + + @property + def orders(self) -> OrdersType: + """Get the orders from the shared state.""" + return self.shared_state.orders + + @orders.setter + def orders(self, orders: OrdersType) -> None: + """Set the orders to the shared state.""" + self.shared_state.orders = orders + + def get_orders(self) -> Generator: + """Get the orders from IPFS.""" + if self.orders is None: + # only fetch once per new batch and store in the shared state for future reference + hash_ = self.synchronized_data.backtested_orders_hash + orders = yield from self.get_from_ipfs(hash_, SupportedFiletype.JSON) + self.orders = cast(OrdersType, orders) + + def handle_success(self, tx_id: Optional[str], url: Optional[str]) -> None: + """Handle a successful response.""" + if not tx_id: + err = "The proxy server returned no transaction id for successful transaction!" + self.context.logger.error(err) + + swap_msg = f"Successfully performed swap transaction with id {tx_id}" + swap_msg += f": {url}" if url is not None else "." + self.context.logger.info(swap_msg) + + def handle_error(self, err: str) -> None: + """Handle an error response.""" + err = f"Proxy server failed to settle transaction with message: {err}" + self.context.logger.error(err) + + def handle_unknown_status(self, status: Optional[str]) -> None: + """Handle a response with an unknown status.""" + err = f"Unknown {status=} was received from the transaction settlement proxy server!" + self.context.logger.error(err) + + def handle_response(self, response: Optional[Dict[str, str]]) -> Optional[str]: + """Handle the response from the proxy server.""" + self.context.logger.debug(f"Proxy server {response=}.") + if response is None: + return None + + status = response.get(PROXY_STATUS_FIELD, None) + tx_id = response.get(PROXY_TX_ID_FIELD, None) + if status == PROXY_SUCCESS_RESPONSE: + url = response.get(PROXY_TX_URL_FIELD, None) + self.handle_success(tx_id, url) + elif status == PROXY_ERROR_RESPONSE: + err = response.get(PROXY_ERROR_MESSAGE_FIELD, "") + self.handle_error(err) + else: + self.handle_unknown_status(status) + + return tx_id + + def perform_next_order(self) -> Generator[None, None, Optional[str]]: + """Perform the next order in priority and return the tx id or `None` if not sent.""" + if self.orders is None: + err = "Orders were expected to be set." + self.context.logger.error(err) + return None + + if len(self.orders) == 0: + self.context.logger.info("No more orders to process.") + self.orders = None + return "" + + quote_data = self.orders.pop(0) + msg = f"Attempting to swap {quote_data['inputMint']} -> {quote_data['outputMint']}..." + self.context.logger.info(msg) + + proxy_api = cast(TxSettlementProxy, self.context.tx_settlement_proxy) + # hacky solution + params = proxy_api.get_spec()["parameters"] + quote_data.update(params) + + response = yield from self._get_response(proxy_api, {}, content=quote_data) + return self.handle_response(response) + + def async_act(self) -> Generator: + """Do the action.""" + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + yield from self.get_orders() + sender = self.context.agent_address + tx_id = yield from self.perform_next_order() + payload = SendSwapProxyPayload(sender, tx_id) + + yield from self.finish_behaviour(payload) diff --git a/packages/valory/skills/strategy_evaluator_abci/behaviours/round_behaviour.py b/packages/valory/skills/strategy_evaluator_abci/behaviours/round_behaviour.py new file mode 100644 index 0000000..cf0b67b --- /dev/null +++ b/packages/valory/skills/strategy_evaluator_abci/behaviours/round_behaviour.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the round behaviour for the 'strategy_evaluator_abci' skill.""" + +from typing import Set, Type + +from packages.valory.skills.abstract_round_abci.behaviours import ( + AbstractRoundBehaviour, + BaseBehaviour, +) +from packages.valory.skills.strategy_evaluator_abci.behaviours.backtesting import ( + BacktestBehaviour, +) +from packages.valory.skills.strategy_evaluator_abci.behaviours.prepare_swap_tx import ( + PrepareEvmSwapBehaviour, + PrepareSwapBehaviour, +) +from packages.valory.skills.strategy_evaluator_abci.behaviours.proxy_swap_queue import ( + ProxySwapQueueBehaviour, +) +from packages.valory.skills.strategy_evaluator_abci.behaviours.strategy_exec import ( + StrategyExecBehaviour, +) +from packages.valory.skills.strategy_evaluator_abci.behaviours.swap_queue import ( + SwapQueueBehaviour, +) +from packages.valory.skills.strategy_evaluator_abci.rounds import ( + StrategyEvaluatorAbciApp, +) + + +class AgentStrategyEvaluatorRoundBehaviour(AbstractRoundBehaviour): + """This behaviour manages the consensus stages for the strategy evaluation.""" + + initial_behaviour_cls = StrategyExecBehaviour + abci_app_cls = StrategyEvaluatorAbciApp + behaviours: Set[Type[BaseBehaviour]] = { + StrategyExecBehaviour, # type: ignore + PrepareSwapBehaviour, # type: ignore + PrepareEvmSwapBehaviour, # type: ignore + SwapQueueBehaviour, # type: ignore + ProxySwapQueueBehaviour, # type: ignore + BacktestBehaviour, # type: ignore + } diff --git a/packages/valory/skills/strategy_evaluator_abci/behaviours/strategy_exec.py b/packages/valory/skills/strategy_evaluator_abci/behaviours/strategy_exec.py new file mode 100644 index 0000000..4e9e54c --- /dev/null +++ b/packages/valory/skills/strategy_evaluator_abci/behaviours/strategy_exec.py @@ -0,0 +1,351 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the behaviour for executing a strategy.""" + +from typing import Any, Dict, Generator, List, Optional, Tuple, cast + +from packages.valory.skills.abstract_round_abci.io_.store import SupportedFiletype +from packages.valory.skills.portfolio_tracker_abci.behaviours import SOL_ADDRESS +from packages.valory.skills.strategy_evaluator_abci.behaviours.base import ( + CALLABLE_KEY, + StrategyEvaluatorBaseBehaviour, +) +from packages.valory.skills.strategy_evaluator_abci.models import AMOUNT_PARAM +from packages.valory.skills.strategy_evaluator_abci.states.strategy_exec import ( + StrategyExecRound, +) + + +STRATEGY_KEY = "trading_strategy" +PRICE_DATA_KEY = "price_data" +TRANSFORMED_PRICE_DATA_KEY = "transformed_data" +TOKEN_ID_KEY = "token_id" # nosec B105:hardcoded_password_string +PORTFOLIO_DATA_KEY = "portfolio_data" +SWAP_DECISION_FIELD = "signal" +BUY_DECISION = "buy" +SELL_DECISION = "sell" +HODL_DECISION = "hold" +AVAILABLE_DECISIONS = (BUY_DECISION, SELL_DECISION, HODL_DECISION) +NO_SWAP_DECISION = {SWAP_DECISION_FIELD: HODL_DECISION} +SOL = "SOL" +RUN_CALLABLE_KEY = "run_callable" +INPUT_MINT = "inputMint" +OUTPUT_MINT = "outputMint" + + +class StrategyExecBehaviour(StrategyEvaluatorBaseBehaviour): + """A behaviour in which the agents execute the selected strategy and decide on the swap(s).""" + + matching_round = StrategyExecRound + + def __init__(self, **kwargs: Any): + """Initialize the behaviour.""" + super().__init__(**kwargs) + self.sol_balance: int = 0 + self.sol_balance_after_swaps: int = 0 + + def get_swap_amount(self) -> int: + """Get the swap amount.""" + if self.params.use_proxy_server: + api = self.context.tx_settlement_proxy + else: + api = self.context.swap_quotes + + return api.parameters.get(AMOUNT_PARAM, 0) + + def is_balance_sufficient( + self, + token: str, + token_balance: int, + ) -> Optional[bool]: + """Check whether the balance of the given token is enough to perform the swap transaction.""" + if token == SOL_ADDRESS and self.sol_balance_after_swaps <= 0: + warning = "Preceding trades are expected to use up all the SOL. Not taking any action." + self.context.logger.warning(warning) + return False + + swap_cost = self.params.expected_swap_tx_cost + # we set it to `None` if no swaps have been prepared yet + sol_before_swap = ( + None + if self.sol_balance_after_swaps == self.sol_balance + else self.sol_balance_after_swaps + ) + if swap_cost > self.sol_balance_after_swaps: + self.context.logger.warning( + "There is not enough SOL to cover the expected swap tx's cost. " + f"SOL balance after preceding swaps ({self.sol_balance_after_swaps}) < swap cost ({swap_cost}). " + f"Not taking any actions." + ) + return False + self.sol_balance_after_swaps -= swap_cost + + swap_amount = self.get_swap_amount() + if token == SOL_ADDRESS: + # do not use the SOL's address to simplify the log messages + token = SOL + compared_balance = self.sol_balance_after_swaps + self.sol_balance_after_swaps -= swap_amount + else: + compared_balance = token_balance + + self.context.logger.info(f"Balance ({token}): {token_balance}.") + if swap_amount > compared_balance: + warning = ( + f"There is not enough balance to cover the swap amount ({swap_amount}) " + ) + if token == SOL: + # subtract the SOL we'd have before this swap or the token's balance if there are no preceding swaps + preceding_swaps_amount = token_balance - ( + sol_before_swap or token_balance + ) + + warning += ( + f"plus the expected swap tx's cost ({swap_cost}) [" + f"also taking into account preceding swaps' amount ({preceding_swaps_amount})] " + ) + # the swap's cost which was subtracted during the first `get_native_balance` call + # should be included in the swap amount + self.sol_balance_after_swaps += swap_amount + swap_amount += swap_cost + token_balance -= preceding_swaps_amount + self.sol_balance_after_swaps += swap_cost + warning += f"({token_balance} < {swap_amount}) for {token!r}. Not taking any actions." + self.context.logger.warning(warning) + return False + + self.context.logger.info("Balance is sufficient.") + return True + + def get_swap_decision( + self, + token_data: Any, + portfolio_data: Dict[str, int], + token: str, + ) -> Optional[str]: + """Get the swap decision given a token's data.""" + strategy = self.synchronized_data.selected_strategy + self.context.logger.info(f"Using trading strategy {strategy!r}.") + # the following are always passed to a strategy script, which may choose to ignore any + kwargs: Dict[str, Any] = self.params.strategies_kwargs + kwargs.update( + { + STRATEGY_KEY: strategy, + CALLABLE_KEY: RUN_CALLABLE_KEY, + TRANSFORMED_PRICE_DATA_KEY: token_data, + PORTFOLIO_DATA_KEY: portfolio_data, + TOKEN_ID_KEY: token, + } + ) + results = self.execute_strategy_callable(**kwargs) + if results is None: + results = NO_SWAP_DECISION + + self.log_from_strategy_results(results) + decision = results.get(SWAP_DECISION_FIELD, None) + if decision is None: + self.context.logger.error( + f"Required field {SWAP_DECISION_FIELD!r} was not returned by {strategy} strategy." + "Not taking any actions." + ) + if decision not in AVAILABLE_DECISIONS: + self.context.logger.error( + f"Invalid decision {decision!r} was detected! Expected one of {AVAILABLE_DECISIONS}." + "Not taking any actions." + ) + decision = None + + return decision + + def get_token_swap_position(self, decision: str) -> Optional[str]: + """Get the position of the non-native token in the swap operation.""" + token_swap_position = None + + if decision == BUY_DECISION: + token_swap_position = OUTPUT_MINT + elif decision == SELL_DECISION: + token_swap_position = INPUT_MINT + elif decision != HODL_DECISION: + self.context.logger.error( + f"Unrecognised decision {decision!r} found! Expected one of {AVAILABLE_DECISIONS}." + ) + + return token_swap_position + + def get_solana_orders( + self, token_data: Dict[str, Any] + ) -> Generator[None, None, Tuple[List[Dict[str, str]], bool]]: + """Get a mapping from a string indicating whether to buy or sell, to a list of tokens.""" + portfolio = yield from self.get_from_ipfs( + self.synchronized_data.portfolio_hash, SupportedFiletype.JSON + ) + portfolio = cast(Optional[Dict[str, int]], portfolio) + if portfolio is None: + self.context.logger.error("Could not get the portfolio from IPFS.") + # return empty orders and incomplete status, because the portfolio is necessary for all the swaps + return [], True + + sol_balance = portfolio.get(SOL_ADDRESS, None) + if sol_balance is None: + err = "The portfolio data do not contain any information for SOL." + self.context.logger.error(err) + # return empty orders and incomplete status, because SOL are necessary for all the swaps + return [], True + self.sol_balance = self.sol_balance_after_swaps = sol_balance + + orders: List[Dict[str, str]] = [] + incomplete = False + for token, data in token_data.items(): + if token == SOL_ADDRESS: + continue + + decision = self.get_swap_decision(data, portfolio, token) + if decision is None: + incomplete = True + continue + + msg = f"Decided to {decision} token with address {token!r}." + self.context.logger.info(msg) + quote_data = {INPUT_MINT: SOL_ADDRESS, OUTPUT_MINT: SOL_ADDRESS} + token_swap_position = self.get_token_swap_position(decision) + if token_swap_position is None: + # holding token, no tx to perform + continue + + quote_data[token_swap_position] = token + input_token = quote_data[INPUT_MINT] + if input_token is not SOL: + token_balance = portfolio.get(input_token, None) + if token_balance is None: + err = f"The portfolio data do not contain any information for {token!r}." + self.context.logger.error(err) + # return, because a swap for another token might be performed + continue + else: + token_balance = self.sol_balance + + enough_tokens = self.is_balance_sufficient(input_token, token_balance) + if not enough_tokens: + incomplete = True + continue + orders.append(quote_data) + + # we only yield here to convert this method to a generator, so that it can be used by `get_process_store_act` + yield + return orders, incomplete + + def get_evm_orders( + self, + token_data: Dict[str, Any], + required_amount: int = 0.00001, + ) -> Generator[None, None, Tuple[List[Dict[str, str]], bool]]: + """Get a mapping from a string indicating whether to buy or sell, to a list of tokens.""" + # We need to check if the portfolio contains any information for the NATIVE_TOKEN which is yet to be defined + # We will temporarily skip this check + # TODO: Define NATIVE_TOKEN, BASE_TOKEN, and LEDGER_ID + # TODO: Check if the portfolio contains any information for the NATIVE_TOKEN + # TODO: Mapping of ledger id to base token + # TODO: update evm balance checker to include native token balance in the portfolio. + + portfolio: Optional[Dict[str, int]] = yield from self.get_from_ipfs( # type: ignore + self.synchronized_data.portfolio_hash, SupportedFiletype.JSON + ) + ledger_id: str = self.params.ledger_ids[0] + base_token = self.params.base_tokens.get(ledger_id, None) + if base_token is None: + self.context.logger.error( + f"Could not get the base token for ledger {ledger_id!r} from the configuration." + ) + return [], True + native_token = self.params.native_currencies.get(ledger_id, None) + if native_token is None: + self.context.logger.error( + f"Could not get the native token for ledger {ledger_id!r} from the configuration." + ) + return [], True + + if portfolio is None: + self.context.logger.error("Could not get the portfolio from IPFS.") + # return empty orders and incomplete status, because the portfolio is necessary for all the swaps + return [], True + + native_balance = portfolio.get(native_token, None) + if native_balance is None: + err = f"The portfolio data do not contain any information for the native {native_token!r} on ledger {ledger_id!r}." + self.context.logger.error(err) + # return empty orders and incomplete status, because SOL are necessary for all the swaps + return [], True + + orders: List[Dict[str, str]] = [] + incomplete = False + for token, data in token_data.items(): + if token == base_token or token == native_token: + continue + + decision = self.get_swap_decision(data, portfolio, token) + if decision is None: + incomplete = True + continue + + msg = f"Decided to {decision} token with address {token!r}." + self.context.logger.info(msg) + token_swap_position = self.get_token_swap_position(decision) + if token_swap_position is None: + # holding token, no tx to perform + continue + + input_token = base_token + output_token = token + + input_balance = portfolio.get(input_token, None) + if input_balance is None: + err = f"The portfolio does not contain information for the base token {base_token!r}. The base token is required for all swaps." + self.context.logger.error(err) + # return, because a swap for another token might be performed + continue + + if input_token == native_token: + incomplete = True + continue + + if input_balance < int(required_amount): + err = f"The portfolio does not contain enough balance for the base token {base_token!r}. Current balance: {input_balance}. Required amount: {required_amount}." + self.context.logger.error(err) + breakpoint() + incomplete = True + continue + quote_data = {INPUT_MINT: input_token, OUTPUT_MINT: output_token} + orders.append(quote_data) + + # we only yield here to convert this method to a generator, so that it can be used by `get_process_store_act` + yield + return orders, incomplete + + def async_act(self) -> Generator: + """Do the action.""" + # We check if we are processing the solana or the ethereum chain + processing_function = ( + self.get_solana_orders if self.params.use_solana else self.get_evm_orders + ) + yield from self.get_process_store_act( + self.synchronized_data.transformed_data_hash, + processing_function, # type: ignore + str(self.swap_decision_filepath), + ) diff --git a/packages/valory/skills/strategy_evaluator_abci/behaviours/swap_queue.py b/packages/valory/skills/strategy_evaluator_abci/behaviours/swap_queue.py new file mode 100644 index 0000000..daa680f --- /dev/null +++ b/packages/valory/skills/strategy_evaluator_abci/behaviours/swap_queue.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the behaviour for preparing a transaction for the next swap in the queue of instructions.""" + +import json +from typing import Any, Dict, Generator, List, Optional, cast + +from packages.valory.skills.abstract_round_abci.io_.store import SupportedFiletype +from packages.valory.skills.strategy_evaluator_abci.behaviours.base import ( + StrategyEvaluatorBaseBehaviour, +) +from packages.valory.skills.strategy_evaluator_abci.payloads import SendSwapPayload +from packages.valory.skills.strategy_evaluator_abci.states.swap_queue import ( + SwapQueueRound, +) + + +class SwapQueueBehaviour(StrategyEvaluatorBaseBehaviour): + """A behaviour in which the agents prepare a transaction for the next swap in the queue of instructions.""" + + matching_round = SwapQueueRound + + @property + def instructions(self) -> Optional[List[Dict[str, Any]]]: + """Get the instructions from the shared state.""" + return self.shared_state.instructions + + @instructions.setter + def instructions(self, instructions: Optional[List[Dict[str, Any]]]) -> None: + """Set the instructions to the shared state.""" + self.shared_state.instructions = instructions + + def get_instructions(self) -> Generator: + """Get the instructions from IPFS.""" + if self.instructions is None: + # only fetch once per new queue and store in the shared state for future reference + hash_ = self.synchronized_data.instructions_hash + instructions = yield from self.get_from_ipfs(hash_, SupportedFiletype.JSON) + self.instructions = cast(Optional[List[Dict[str, Any]]], instructions) + + def get_next_instructions(self) -> Optional[str]: + """Return the next instructions in priority serialized or `None` if there are no instructions left.""" + if self.instructions is None: + err = "Instructions were expected to be set." + self.context.logger.error(err) + return None + + if len(self.instructions) == 0: + self.context.logger.info("No more instructions to process.") + self.instructions = None + return "" + + instructions = self.instructions.pop(0) + if len(instructions) == 0: + err = "The next instructions in priority are not correctly set! Skipping them..." + self.context.logger.error(err) + return None + + try: + return json.dumps(instructions) + except (json.decoder.JSONDecodeError, TypeError): + err = "The next instructions in priority are not correctly formatted! Skipping them..." + self.context.logger.error(err) + return None + + def async_act(self) -> Generator: + """Do the action.""" + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + yield from self.get_instructions() + sender = self.context.agent_address + serialized_instructions = self.get_next_instructions() + payload = SendSwapPayload(sender, serialized_instructions) + + yield from self.finish_behaviour(payload) diff --git a/packages/valory/skills/strategy_evaluator_abci/dialogues.py b/packages/valory/skills/strategy_evaluator_abci/dialogues.py new file mode 100644 index 0000000..16b98c0 --- /dev/null +++ b/packages/valory/skills/strategy_evaluator_abci/dialogues.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the classes required for dialogue management.""" + +from packages.eightballer.protocols.orders.dialogues import ( + OrdersDialogue as BaseOrdersDialogue, +) +from packages.eightballer.protocols.orders.dialogues import ( + OrdersDialogues as BaseOrdersDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + AbciDialogue as BaseAbciDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + AbciDialogues as BaseAbciDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + ContractApiDialogue as BaseContractApiDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + ContractApiDialogues as BaseContractApiDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + HttpDialogue as BaseHttpDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + HttpDialogues as BaseHttpDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + IpfsDialogue as BaseIpfsDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + IpfsDialogues as BaseIpfsDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + LedgerApiDialogue as BaseLedgerApiDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + LedgerApiDialogues as BaseLedgerApiDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + SigningDialogue as BaseSigningDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + SigningDialogues as BaseSigningDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + TendermintDialogue as BaseTendermintDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + TendermintDialogues as BaseTendermintDialogues, +) + + +AbciDialogue = BaseAbciDialogue +AbciDialogues = BaseAbciDialogues + + +HttpDialogue = BaseHttpDialogue +HttpDialogues = BaseHttpDialogues + + +SigningDialogue = BaseSigningDialogue +SigningDialogues = BaseSigningDialogues + + +LedgerApiDialogue = BaseLedgerApiDialogue +LedgerApiDialogues = BaseLedgerApiDialogues + + +ContractApiDialogue = BaseContractApiDialogue +ContractApiDialogues = BaseContractApiDialogues + + +TendermintDialogue = BaseTendermintDialogue +TendermintDialogues = BaseTendermintDialogues + + +IpfsDialogue = BaseIpfsDialogue +IpfsDialogues = BaseIpfsDialogues + +DcxtOrdersDialogue = BaseOrdersDialogue +DcxtOrdersDialogues = BaseOrdersDialogues diff --git a/packages/valory/skills/strategy_evaluator_abci/fsm_specification.yaml b/packages/valory/skills/strategy_evaluator_abci/fsm_specification.yaml new file mode 100644 index 0000000..4576d28 --- /dev/null +++ b/packages/valory/skills/strategy_evaluator_abci/fsm_specification.yaml @@ -0,0 +1,85 @@ +alphabet_in: +- BACKTEST_FAILED +- BACKTEST_NEGATIVE +- BACKTEST_POSITIVE +- BACKTEST_POSITIVE_EVM +- BACKTEST_POSITIVE_PROXY_SERVER +- ERROR_BACKTESTING +- ERROR_PREPARING_INSTRUCTIONS +- ERROR_PREPARING_SWAPS +- INCOMPLETE_INSTRUCTIONS_PREPARED +- INSTRUCTIONS_PREPARED +- NO_INSTRUCTIONS +- NO_MAJORITY +- NO_ORDERS +- PREPARE_INCOMPLETE_SWAP +- PREPARE_SWAP +- PROXY_SWAPPED +- PROXY_SWAP_FAILED +- PROXY_SWAP_TIMEOUT +- ROUND_TIMEOUT +- SWAPS_QUEUE_EMPTY +- SWAP_TX_PREPARED +- TRANSACTION_PREPARED +- TX_PREPARATION_FAILED +default_start_state: StrategyExecRound +final_states: +- BacktestingFailedRound +- BacktestingNegativeRound +- HodlRound +- InstructionPreparationFailedRound +- NoMoreSwapsRound +- StrategyExecutionFailedRound +- SwapTxPreparedRound +label: StrategyEvaluatorAbciApp +start_states: +- StrategyExecRound +states: +- BacktestRound +- BacktestingFailedRound +- BacktestingNegativeRound +- HodlRound +- InstructionPreparationFailedRound +- NoMoreSwapsRound +- PrepareEvmSwapRound +- PrepareSwapRound +- ProxySwapQueueRound +- StrategyExecRound +- StrategyExecutionFailedRound +- SwapQueueRound +- SwapTxPreparedRound +transition_func: + (BacktestRound, BACKTEST_FAILED): BacktestingFailedRound + (BacktestRound, BACKTEST_NEGATIVE): BacktestingNegativeRound + (BacktestRound, BACKTEST_POSITIVE): PrepareSwapRound + (BacktestRound, BACKTEST_POSITIVE_EVM): PrepareEvmSwapRound + (BacktestRound, BACKTEST_POSITIVE_PROXY_SERVER): ProxySwapQueueRound + (BacktestRound, ERROR_BACKTESTING): BacktestingFailedRound + (BacktestRound, NO_MAJORITY): BacktestRound + (BacktestRound, ROUND_TIMEOUT): BacktestRound + (PrepareEvmSwapRound, ROUND_TIMEOUT): PrepareEvmSwapRound + (PrepareEvmSwapRound, NO_INSTRUCTIONS): PrepareEvmSwapRound + (PrepareEvmSwapRound, TRANSACTION_PREPARED): SwapTxPreparedRound + (PrepareEvmSwapRound, NO_MAJORITY): PrepareEvmSwapRound + (PrepareSwapRound, ERROR_PREPARING_INSTRUCTIONS): InstructionPreparationFailedRound + (PrepareSwapRound, INCOMPLETE_INSTRUCTIONS_PREPARED): SwapQueueRound + (PrepareSwapRound, INSTRUCTIONS_PREPARED): SwapQueueRound + (PrepareSwapRound, NO_INSTRUCTIONS): HodlRound + (PrepareSwapRound, NO_MAJORITY): PrepareSwapRound + (PrepareSwapRound, ROUND_TIMEOUT): PrepareSwapRound + (ProxySwapQueueRound, NO_MAJORITY): ProxySwapQueueRound + (ProxySwapQueueRound, PROXY_SWAPPED): ProxySwapQueueRound + (ProxySwapQueueRound, PROXY_SWAP_FAILED): ProxySwapQueueRound + (ProxySwapQueueRound, PROXY_SWAP_TIMEOUT): ProxySwapQueueRound + (ProxySwapQueueRound, SWAPS_QUEUE_EMPTY): NoMoreSwapsRound + (StrategyExecRound, ERROR_PREPARING_SWAPS): StrategyExecutionFailedRound + (StrategyExecRound, NO_MAJORITY): StrategyExecRound + (StrategyExecRound, NO_ORDERS): HodlRound + (StrategyExecRound, PREPARE_INCOMPLETE_SWAP): BacktestRound + (StrategyExecRound, PREPARE_SWAP): BacktestRound + (StrategyExecRound, ROUND_TIMEOUT): StrategyExecRound + (SwapQueueRound, NO_MAJORITY): SwapQueueRound + (SwapQueueRound, ROUND_TIMEOUT): SwapQueueRound + (SwapQueueRound, SWAPS_QUEUE_EMPTY): NoMoreSwapsRound + (SwapQueueRound, SWAP_TX_PREPARED): SwapTxPreparedRound + (SwapQueueRound, TX_PREPARATION_FAILED): SwapQueueRound diff --git a/packages/valory/skills/strategy_evaluator_abci/handlers.py b/packages/valory/skills/strategy_evaluator_abci/handlers.py new file mode 100644 index 0000000..5593cc5 --- /dev/null +++ b/packages/valory/skills/strategy_evaluator_abci/handlers.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the handler for the 'strategy_evaluator_abci' skill.""" + +from packages.eightballer.protocols.orders.message import OrdersMessage +from packages.valory.skills.abstract_round_abci.handlers import ( + ABCIRoundHandler as BaseABCIRoundHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import AbstractResponseHandler +from packages.valory.skills.abstract_round_abci.handlers import ( + ContractApiHandler as BaseContractApiHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + HttpHandler as BaseHttpHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + IpfsHandler as BaseIpfsHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + LedgerApiHandler as BaseLedgerApiHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + SigningHandler as BaseSigningHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + TendermintHandler as BaseTendermintHandler, +) + + +class DcxtOrdersHandler(AbstractResponseHandler): + """This class implements a handler for DexTickersHandler messages.""" + + SUPPORTED_PROTOCOL = OrdersMessage.protocol_id + allowed_response_performatives = frozenset( + { + OrdersMessage.Performative.GET_ORDERS, + OrdersMessage.Performative.CREATE_ORDER, + OrdersMessage.Performative.ORDER_CREATED, + OrdersMessage.Performative.ERROR, + } + ) + + +ABCIHandler = BaseABCIRoundHandler +HttpHandler = BaseHttpHandler +SigningHandler = BaseSigningHandler +LedgerApiHandler = BaseLedgerApiHandler +ContractApiHandler = BaseContractApiHandler +TendermintHandler = BaseTendermintHandler +IpfsHandler = BaseIpfsHandler diff --git a/packages/valory/skills/strategy_evaluator_abci/models.py b/packages/valory/skills/strategy_evaluator_abci/models.py new file mode 100644 index 0000000..5a71132 --- /dev/null +++ b/packages/valory/skills/strategy_evaluator_abci/models.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the models for the skill.""" + +from typing import Any, Dict, Iterable, List, Optional, Tuple, Type + +from aea.skills.base import SkillContext + +from packages.valory.skills.abstract_round_abci.base import AbciApp +from packages.valory.skills.abstract_round_abci.models import ApiSpecs, BaseParams +from packages.valory.skills.abstract_round_abci.models import ( + BenchmarkTool as BaseBenchmarkTool, +) +from packages.valory.skills.abstract_round_abci.models import Requests as BaseRequests +from packages.valory.skills.abstract_round_abci.models import ( + SharedState as BaseSharedState, +) +from packages.valory.skills.strategy_evaluator_abci.rounds import ( + StrategyEvaluatorAbciApp, +) + + +Requests = BaseRequests +BenchmarkTool = BaseBenchmarkTool + + +AMOUNT_PARAM = "amount" +SLIPPAGE_PARAM = "slippageBps" + + +class SharedState(BaseSharedState): + """Keep the current shared state of the skill.""" + + abci_app_cls: Type[AbciApp] = StrategyEvaluatorAbciApp + + def __init__(self, *args: Any, skill_context: SkillContext, **kwargs: Any) -> None: + """Initialize the state.""" + super().__init__(*args, skill_context=skill_context, **kwargs) + # utilized if using the proxy server + self.orders: Optional[List[Dict[str, str]]] = None + # utilized if using the Solana tx settlement + self.instructions: Optional[List[Dict[str, Any]]] = None + + def setup(self) -> None: + """Set up the model.""" + super().setup() + if ( + self.context.params.use_proxy_server + and self.synchronized_data.max_participants != 1 + ): + raise ValueError("Cannot use proxy server with a multi-agent service!") + + swap_apis: Tuple[ApiSpecs, ApiSpecs] = ( + self.context.swap_quotes, + self.context.tx_settlement_proxy, + ) + required_swap_params = (AMOUNT_PARAM, SLIPPAGE_PARAM) + for swap_api in swap_apis: + for swap_param in required_swap_params: + if swap_param not in swap_api.parameters: + exc = f"Api with id {swap_api.api_id!r} missing required parameter: {swap_param}!" + raise ValueError(exc) + + amounts = (api.parameters[AMOUNT_PARAM] for api in swap_apis) + expected_swap_tx_cost = self.context.params.expected_swap_tx_cost + if any(expected_swap_tx_cost > amount for amount in amounts): + exc = "The expected cost of the swap transaction cannot be greater than the swap amount!" + raise ValueError(exc) + + +def _raise_incorrect_config(key: str, values: Any) -> None: + """Raise a `ValueError` for incorrect configuration of a nested_list workaround.""" + raise ValueError( + f"The given configuration for {key!r} is incorrectly formatted: {values}!" + "The value is expected to be a list of lists that can be represented as a dictionary." + ) + + +def nested_list_todict_workaround( + kwargs: Dict, + key: str, +) -> Dict: + """Get a nested list from the kwargs and convert it to a dictionary.""" + values = list(kwargs.get(key, [])) + if len(values) == 0: + raise ValueError(f"No {key!r} specified in agent's configurations: {kwargs}!") + if any(not issubclass(type(nested_values), Iterable) for nested_values in values): + _raise_incorrect_config(key, values) + if any(len(nested_values) % 2 == 1 for nested_values in values): + _raise_incorrect_config(key, values) + return {value[0]: value[1] for value in values} + + +class StrategyEvaluatorParams(BaseParams): + """Strategy evaluator's parameters.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize the parameters' object.""" + self.strategies_kwargs: Dict[str, List[Any]] = nested_list_todict_workaround( + kwargs, "strategies_kwargs" + ) + self.use_proxy_server: bool = self._ensure("use_proxy_server", kwargs, bool) + self.proxy_round_timeout_seconds: float = self._ensure( + "proxy_round_timeout_seconds", kwargs, float + ) + self.expected_swap_tx_cost: int = self._ensure( + "expected_swap_tx_cost", kwargs, int + ) + self.ipfs_fetch_retries: int = self._ensure("ipfs_fetch_retries", kwargs, int) + self.sharpe_threshold: float = self._ensure("sharpe_threshold", kwargs, float) + self.use_solana = self._ensure("use_solana", kwargs, bool) + self.base_tokens = self._ensure("base_tokens", kwargs, Dict[str, str]) + self.native_currencies = self._ensure( + "native_currencies", kwargs, Dict[str, str] + ) + self.trade_size_in_base_token = self._ensure( + "trade_size_in_base_token", kwargs, float + ) + super().__init__(*args, **kwargs) + + +class SwapQuotesSpecs(ApiSpecs): + """A model that wraps ApiSpecs for the Jupiter quotes specifications.""" + + +class SwapInstructionsSpecs(ApiSpecs): + """A model that wraps ApiSpecs for the Jupiter instructions specifications.""" + + +class TxSettlementProxy(ApiSpecs): + """A model that wraps ApiSpecs for the Solana transaction settlement proxy server.""" diff --git a/packages/valory/skills/strategy_evaluator_abci/payloads.py b/packages/valory/skills/strategy_evaluator_abci/payloads.py new file mode 100644 index 0000000..e2cd549 --- /dev/null +++ b/packages/valory/skills/strategy_evaluator_abci/payloads.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the transaction payloads for the strategy evaluator.""" + +from dataclasses import dataclass +from typing import Optional + +from packages.valory.skills.abstract_round_abci.base import BaseTxPayload + + +@dataclass(frozen=True) +class IPFSHashPayload(BaseTxPayload): + """Represents a transaction payload for an IPFS hash.""" + + ipfs_hash: Optional[str] + incomplete: Optional[bool] + + +@dataclass(frozen=True) +class SendSwapProxyPayload(BaseTxPayload): + """Represents a transaction payload for attempting a swap transaction via the proxy server.""" + + tx_id: Optional[str] + + +@dataclass(frozen=True) +class SendSwapPayload(BaseTxPayload): + """Represents a transaction payload for preparing the instruction for a swap transaction.""" + + # `instructions` is a serialized `List[Dict[str, Any]]` + instructions: Optional[str] + + +@dataclass(frozen=True) +class TransactionHashPayload(BaseTxPayload): + """Represent a transaction payload of type 'tx_hash'.""" + + tx_hash: Optional[str] diff --git a/packages/valory/skills/strategy_evaluator_abci/rounds.py b/packages/valory/skills/strategy_evaluator_abci/rounds.py new file mode 100644 index 0000000..5d2c057 --- /dev/null +++ b/packages/valory/skills/strategy_evaluator_abci/rounds.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the rounds for the strategy evaluator.""" + +from typing import Dict, Set + +from packages.valory.skills.abstract_round_abci.base import ( + AbciApp, + AbciAppTransitionFunction, + AppState, + get_name, +) +from packages.valory.skills.strategy_evaluator_abci.states.backtesting import ( + BacktestRound, +) +from packages.valory.skills.strategy_evaluator_abci.states.base import ( + Event, + SynchronizedData, +) +from packages.valory.skills.strategy_evaluator_abci.states.final_states import ( + BacktestingFailedRound, + BacktestingNegativeRound, + HodlRound, + InstructionPreparationFailedRound, + NoMoreSwapsRound, + StrategyExecutionFailedRound, + SwapTxPreparedRound, +) +from packages.valory.skills.strategy_evaluator_abci.states.prepare_swap import ( + PrepareEvmSwapRound, + PrepareSwapRound, +) +from packages.valory.skills.strategy_evaluator_abci.states.proxy_swap_queue import ( + ProxySwapQueueRound, +) +from packages.valory.skills.strategy_evaluator_abci.states.strategy_exec import ( + StrategyExecRound, +) +from packages.valory.skills.strategy_evaluator_abci.states.swap_queue import ( + SwapQueueRound, +) + + +class StrategyEvaluatorAbciApp(AbciApp[Event]): + """StrategyEvaluatorAbciApp + + Initial round: StrategyExecRound + + Initial states: {StrategyExecRound} + + Transition states: + 0. StrategyExecRound + - prepare swap: 1. + - prepare incomplete swap: 1. + - no orders: 12. + - error preparing swaps: 8. + - no majority: 0. + - round timeout: 0. + 1. BacktestRound + - backtest succeeded: 2. + - prepare swap proxy server: 4. + - prepare swap evm: 5. + - backtest negative: 9. + - backtest failed: 10. + - error backtesting: 10. + - no majority: 1. + - round timeout: 1. + 2. PrepareSwapRound + - instructions prepared: 3. + - incomplete instructions prepared: 3. + - no instructions: 12. + - error preparing instructions: 11. + - no majority: 2. + - round timeout: 2. + 3. SwapQueueRound + - swap tx prepared: 6. + - swaps queue empty: 7. + - none: 3. + - no majority: 3. + - round timeout: 3. + 4. ProxySwapQueueRound + - proxy swapped: 4. + - swaps queue empty: 7. + - proxy swap failed: 4. + - no majority: 4. + - proxy swap timeout: 4. + 5. PrepareEvmSwapRound + - transaction prepared: 6. + - round timeout: 5. + - no instructions: 5. + - no majority: 5. + 6. SwapTxPreparedRound + 7. NoMoreSwapsRound + 8. StrategyExecutionFailedRound + 9. BacktestingNegativeRound + 10. BacktestingFailedRound + 11. InstructionPreparationFailedRound + 12. HodlRound + + Final states: {BacktestingFailedRound, BacktestingNegativeRound, HodlRound, InstructionPreparationFailedRound, NoMoreSwapsRound, StrategyExecutionFailedRound, SwapTxPreparedRound} + + Timeouts: + round timeout: 30.0 + proxy swap timeout: 1200.0 + """ + + initial_round_cls: AppState = StrategyExecRound + initial_states: Set[AppState] = {StrategyExecRound} + final_states: Set[AppState] = { + SwapTxPreparedRound, + NoMoreSwapsRound, + StrategyExecutionFailedRound, + InstructionPreparationFailedRound, + HodlRound, + BacktestingNegativeRound, + BacktestingFailedRound, + } + event_to_timeout: Dict[Event, float] = { + Event.ROUND_TIMEOUT: 30.0, + Event.PROXY_SWAP_TIMEOUT: 1200.0, + } + db_pre_conditions: Dict[AppState, Set[str]] = { + StrategyExecRound: { + get_name(SynchronizedData.selected_strategy), + get_name(SynchronizedData.data_hash), + }, + } + transition_function: AbciAppTransitionFunction = { + StrategyExecRound: { + Event.PREPARE_SWAP: BacktestRound, + Event.PREPARE_INCOMPLETE_SWAP: BacktestRound, + Event.NO_ORDERS: HodlRound, + Event.ERROR_PREPARING_SWAPS: StrategyExecutionFailedRound, + Event.NO_MAJORITY: StrategyExecRound, + Event.ROUND_TIMEOUT: StrategyExecRound, + }, + BacktestRound: { + Event.BACKTEST_POSITIVE: PrepareSwapRound, + Event.BACKTEST_POSITIVE_PROXY_SERVER: ProxySwapQueueRound, + Event.BACKTEST_POSITIVE_EVM: PrepareEvmSwapRound, + Event.BACKTEST_NEGATIVE: BacktestingNegativeRound, + Event.BACKTEST_FAILED: BacktestingFailedRound, + Event.ERROR_BACKTESTING: BacktestingFailedRound, + Event.NO_MAJORITY: BacktestRound, + Event.ROUND_TIMEOUT: BacktestRound, + }, + PrepareSwapRound: { + Event.INSTRUCTIONS_PREPARED: SwapQueueRound, + Event.INCOMPLETE_INSTRUCTIONS_PREPARED: SwapQueueRound, + Event.NO_INSTRUCTIONS: HodlRound, + Event.ERROR_PREPARING_INSTRUCTIONS: InstructionPreparationFailedRound, + Event.NO_MAJORITY: PrepareSwapRound, + Event.ROUND_TIMEOUT: PrepareSwapRound, + }, + SwapQueueRound: { + Event.SWAP_TX_PREPARED: SwapTxPreparedRound, + Event.SWAPS_QUEUE_EMPTY: NoMoreSwapsRound, + Event.TX_PREPARATION_FAILED: SwapQueueRound, + Event.NO_MAJORITY: SwapQueueRound, + Event.ROUND_TIMEOUT: SwapQueueRound, + }, + ProxySwapQueueRound: { + Event.PROXY_SWAPPED: ProxySwapQueueRound, + Event.SWAPS_QUEUE_EMPTY: NoMoreSwapsRound, + Event.PROXY_SWAP_FAILED: ProxySwapQueueRound, + Event.NO_MAJORITY: ProxySwapQueueRound, + Event.PROXY_SWAP_TIMEOUT: ProxySwapQueueRound, + }, + PrepareEvmSwapRound: { + Event.TRANSACTION_PREPARED: SwapTxPreparedRound, + Event.ROUND_TIMEOUT: PrepareEvmSwapRound, + Event.NO_INSTRUCTIONS: PrepareEvmSwapRound, + Event.NO_MAJORITY: PrepareEvmSwapRound, + }, + SwapTxPreparedRound: {}, + NoMoreSwapsRound: {}, + StrategyExecutionFailedRound: {}, + BacktestingNegativeRound: {}, + BacktestingFailedRound: {}, + InstructionPreparationFailedRound: {}, + HodlRound: {}, + } + db_post_conditions: Dict[AppState, Set[str]] = { + SwapTxPreparedRound: {get_name(SynchronizedData.most_voted_tx_hash)}, + NoMoreSwapsRound: set(), + StrategyExecutionFailedRound: set(), + BacktestingNegativeRound: set(), + BacktestingFailedRound: set(), + InstructionPreparationFailedRound: set(), + HodlRound: set(), + } diff --git a/packages/valory/skills/strategy_evaluator_abci/skill.yaml b/packages/valory/skills/strategy_evaluator_abci/skill.yaml new file mode 100644 index 0000000..f77f468 --- /dev/null +++ b/packages/valory/skills/strategy_evaluator_abci/skill.yaml @@ -0,0 +1,257 @@ +name: strategy_evaluator_abci +author: valory +version: 0.1.0 +type: skill +description: This skill is responsible for the execution of the strategy and preparing + the swapping transaction. +license: Apache-2.0 +aea_version: '>=1.0.0, <2.0.0' +fingerprint: + README.md: bafybeicuikjki6yvgwmv6sxudjagjb3bhfsnypna36eceza2mzmqnthuiy + __init__.py: bafybeifhkyp44uusta77c4ths3xixmrtel3ssoq5mhcstcfsclkvirmw3a + behaviours/__init__.py: bafybeihpuluelzi5sbxgxbeks7rfsbcjivhj57cqoshpkxgufqec55s3rq + behaviours/backtesting.py: bafybeihquc6zopikmkzmc27627txajuzwqwcrnj65nnph55wbexk7f3k4u + behaviours/base.py: bafybeidzevmaz3kvnizxt6cdoozj7mrtnbw3pthck372y3xqngju4afvee + behaviours/prepare_swap_tx.py: bafybeibgvkxsssk65dqi44fwfzfpj66ud36jmpaxp7h2kvor37exqgu5jq + behaviours/proxy_swap_queue.py: bafybeihtsgtmh3sv4ptqw6msbuwhf3krgdrlzlu4gjrdqri55wmbxqk7gy + behaviours/round_behaviour.py: bafybeif26u7uapmhvtzdljj3hlvlqmdgb33dw3bif5lhbtpdjeghe7cdfy + behaviours/strategy_exec.py: bafybeiabstmctll3ofblf7lzso3tjkcptop2rgvf7nvsi34cg3rzggazy4 + behaviours/swap_queue.py: bafybeifuw22ri5vmso2krsspuahhbjzj4cm5v7bbbs2i6tqegw2aohvxby + dialogues.py: bafybeigxdc3i2wq5hp266op5lyyfswwbosphqzrcapgqxscrznggrllype + fsm_specification.yaml: bafybeifkf2mffocii4jspbjcx7wc5ji2mypksfil7ddgebiid2bi55pvfy + handlers.py: bafybeihwqu65rsc5lhixfnhxgocxcn7synhmw66cbzqrfmuevbtkvvwbae + models.py: bafybeictphbbro6hxbyf4waeg55vw5ytts3hwz4wxjuaructwuhu3ehpga + payloads.py: bafybeicpya7e2bedbgohbf4nbxiqi2jw2hrlp2keiwkcm6u6oxexvy2rsy + rounds.py: bafybeiegmi3xzdpvxld2ufkbrylzyl5g2xhjmxw3brw5ndgmrwo4cotdcy + states/__init__.py: bafybeidgoz7uxrcafbiq5mfg6tl7fcmxmd5auu3lexl7cxpbhlxfvfnoom + states/backtesting.py: bafybeid6hsayprjbkzfhfvkbw4azs3v3cf7u74vdi465ywmy2j5chfctnu + states/base.py: bafybeib2oej2sir7ufik4ukdgndq4o427yjmx6bmgqxoyiuhaarukxflti + states/final_states.py: bafybeigkrwvzg2o3exdiofxmunjn2kgolnnojiik7ckzi26dgn2ydh7mla + states/prepare_swap.py: bafybeihzb3muxzh7eedzkb45djizu5ayxxi3rovfc3rc4sqvyms5ufkcrm + states/proxy_swap_queue.py: bafybeidguarl5aoqlgc56df2v2xt6u7rsxij3aymeanfstjv2jsn3jtiay + states/strategy_exec.py: bafybeig72frgbgtxt3zccibv4ztnwvoydcfjmphipordf7gtf25xzk7ilq + states/swap_queue.py: bafybeihtnpowhqoym5lmpiob6wt5ejynnsfjg4j5usutohat4eyq54fzki +fingerprint_ignore_patterns: [] +connections: +- eightballer/dcxt:0.1.0:bafybeihes2id6qmqqybvdqfpchu6h3i5mpnhq6lztfxp52twd22iwqxacq +contracts: +- valory/gnosis_safe:0.1.0:bafybeiho6sbfts3zk3mftrngw37d5qnlvkqtnttt3fzexmcwkeevhu4wwi +protocols: +- eightballer/orders:0.1.0:bafybeibprhniaoq3y2uzc4arwwl7yws3i54ahaicrphh5gtl4xxhxqexdy +- valory/contract_api:1.0.0:bafybeidgu7o5llh26xp3u3ebq3yluull5lupiyeu6iooi2xyymdrgnzq5i +skills: +- valory/abstract_round_abci:0.1.0:bafybeibovsktd3uxur45nrcomq5shcn46cgxd5idmhxbmjhg32c5abyqim +- valory/market_data_fetcher_abci:0.1.0:bafybeia3kld7ogbaolbxskys7r5ccolhm53fqi4tdkrwnvilfm7gn5ztcm +- valory/trader_decision_maker_abci:0.1.0:bafybeih7duxvbjevmeez4bvdpahgvefoik3hnpjq3ik5g4jaqbuw5rybtu +- valory/portfolio_tracker_abci:0.1.0:bafybeigzyhm3fzoxhggjdexryzqgskafoi6rec4ois34n3asodxn6j3txm +- valory/transaction_settlement_abci:0.1.0:bafybeihq2yenstblmaadzcjousowj5kfn5l7ns5pxweq2gcrsczfyq5wzm +behaviours: + main: + args: {} + class_name: AgentStrategyEvaluatorRoundBehaviour +handlers: + abci: + args: {} + class_name: ABCIHandler + contract_api: + args: {} + class_name: ContractApiHandler + http: + args: {} + class_name: HttpHandler + ipfs: + args: {} + class_name: IpfsHandler + ledger_api: + args: {} + class_name: LedgerApiHandler + signing: + args: {} + class_name: SigningHandler + tendermint: + args: {} + class_name: TendermintHandler + orders: + args: {} + class_name: DcxtOrdersHandler +models: + abci_dialogues: + args: {} + class_name: AbciDialogues + benchmark_tool: + args: + log_dir: /logs + class_name: BenchmarkTool + contract_api_dialogues: + args: {} + class_name: ContractApiDialogues + http_dialogues: + args: {} + class_name: HttpDialogues + ipfs_dialogues: + args: {} + class_name: IpfsDialogues + ledger_api_dialogues: + args: {} + class_name: LedgerApiDialogues + params: + args: + cleanup_history_depth: 1 + cleanup_history_depth_current: null + drand_public_key: 868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31 + genesis_config: + genesis_time: '2022-05-20T16:00:21.735122717Z' + chain_id: chain-c4daS1 + consensus_params: + block: + max_bytes: '22020096' + max_gas: '-1' + time_iota_ms: '1000' + evidence: + max_age_num_blocks: '100000' + max_age_duration: '172800000000000' + max_bytes: '1048576' + validator: + pub_key_types: + - ed25519 + version: {} + voting_power: '10' + keeper_timeout: 30.0 + max_attempts: 10 + max_healthcheck: 120 + on_chain_service_id: null + request_retry_delay: 1.0 + request_timeout: 10.0 + reset_pause_duration: 10 + reset_tendermint_after: 2 + retry_attempts: 400 + retry_timeout: 3 + round_timeout_seconds: 350.0 + proxy_round_timeout_seconds: 1200.0 + service_id: decision_maker + service_registry_address: null + agent_registry_address: null + setup: + all_participants: + - '0x0000000000000000000000000000000000000000' + safe_contract_address: '0x0000000000000000000000000000000000000000' + consensus_threshold: null + share_tm_config_on_startup: false + sleep_time: 1 + use_slashing: false + slash_cooldown_hours: 3 + slash_threshold_amount: 10000000000000000 + light_slash_unit_amount: 5000000000000000 + serious_slash_unit_amount: 8000000000000000 + tendermint_check_sleep_delay: 3 + tendermint_com_url: http://localhost:8080 + tendermint_max_retries: 5 + tendermint_p2p_url: localhost:26656 + tendermint_url: http://localhost:26657 + tx_timeout: 10.0 + use_termination: false + strategies_kwargs: + - - extra_1 + - value + - - extra_2 + - value + use_proxy_server: false + use_solana: false + expected_swap_tx_cost: 20000000 + ipfs_fetch_retries: 5 + sharpe_threshold: 1.0 + base_tokens: + ethereum: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' + optimism: '0xda10009cbd5d07dd0cecc66161fc93d7c9000da1' + native_currencies: + ethereum: ETH + optimism: ETH + trade_size_in_base_token: 0.0001 + class_name: StrategyEvaluatorParams + swap_quotes: + args: + api_id: swap_quotes + headers: + Content-Type: application/json + method: GET + parameters: + amount: 100000000 + slippageBps: 5 + response_key: null + response_type: dict + retries: 5 + url: https://quote-api.jup.ag/v6/quote + class_name: SwapQuotesSpecs + swap_instructions: + args: + api_id: swap_instructions + headers: + Content-Type: application/json + method: POST + parameters: {} + response_key: null + response_type: dict + retries: 5 + url: https://quote-api.jup.ag/v6/swap-instructions + class_name: SwapInstructionsSpecs + tx_settlement_proxy: + args: + api_id: tx_settlement_proxy + headers: + Content-Type: application/json + method: POST + parameters: + amount: 100000000 + slippageBps: 5 + resendAmount: 200 + timeoutInMs: 120000 + priorityFee: 5000000 + response_key: null + response_type: dict + retries: 5 + url: http://tx_proxy:3000/tx + class_name: TxSettlementProxy + get_balance: + args: + api_id: get_balance + headers: + Content-Type: application/json + method: POST + parameters: {} + response_key: result:value + response_type: int + error_key: error:message + error_type: str + retries: 5 + url: replace_with_a_solana_rpc + class_name: GetBalance + token_accounts: + args: + api_id: token_accounts + headers: + Content-Type: application/json + method: POST + parameters: {} + response_key: result:value + response_type: list + error_key: error:message + error_type: str + retries: 5 + url: replace_with_a_solana_rpc + class_name: TokenAccounts + requests: + args: {} + class_name: Requests + signing_dialogues: + args: {} + class_name: SigningDialogues + state: + args: {} + class_name: SharedState + tendermint_dialogues: + args: {} + class_name: TendermintDialogues +dependencies: + pyyaml: + version: <=6.0.1,>=3.10 +is_abstract: true diff --git a/packages/valory/skills/strategy_evaluator_abci/states/__init__.py b/packages/valory/skills/strategy_evaluator_abci/states/__init__.py new file mode 100644 index 0000000..ec05cda --- /dev/null +++ b/packages/valory/skills/strategy_evaluator_abci/states/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This package contains the rounds for the 'strategy_evaluator_abci' skill.""" diff --git a/packages/valory/skills/strategy_evaluator_abci/states/backtesting.py b/packages/valory/skills/strategy_evaluator_abci/states/backtesting.py new file mode 100644 index 0000000..c5b0e93 --- /dev/null +++ b/packages/valory/skills/strategy_evaluator_abci/states/backtesting.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the backtesting state of the swap(s).""" + +from typing import Any + +from packages.valory.skills.abstract_round_abci.base import get_name +from packages.valory.skills.strategy_evaluator_abci.states.base import ( + Event, + IPFSRound, + SynchronizedData, +) + + +class BacktestRound(IPFSRound): + """A round in which the agents prepare swap(s) instructions.""" + + done_event = Event.BACKTEST_POSITIVE + incomplete_event = Event.BACKTEST_FAILED + no_hash_event = Event.ERROR_BACKTESTING + none_event = Event.BACKTEST_NEGATIVE + selection_key = ( + get_name(SynchronizedData.backtested_orders_hash), + get_name(SynchronizedData.incomplete_exec), + ) + collection_key = get_name(SynchronizedData.participant_to_backtesting) + + def __init__(self, *args: Any, **kwargs: Any): + """Initialize the strategy execution round.""" + super().__init__(*args, **kwargs) + if self.context.params.use_proxy_server: + self.done_event = Event.BACKTEST_POSITIVE_PROXY_SERVER + # Note, using evm takes precedence over proxy server + if not self.context.params.use_solana: + self.done_event = Event.BACKTEST_POSITIVE_EVM diff --git a/packages/valory/skills/strategy_evaluator_abci/states/base.py b/packages/valory/skills/strategy_evaluator_abci/states/base.py new file mode 100644 index 0000000..bc00b53 --- /dev/null +++ b/packages/valory/skills/strategy_evaluator_abci/states/base.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the base functionality for the rounds of the decision-making abci app.""" + +from enum import Enum +from typing import Optional, Tuple, cast + +from packages.valory.skills.abstract_round_abci.base import ( + BaseSynchronizedData, + CollectSameUntilThresholdRound, + CollectionRound, + DeserializedCollection, +) +from packages.valory.skills.market_data_fetcher_abci.rounds import ( + SynchronizedData as MarketFetcherSyncedData, +) +from packages.valory.skills.portfolio_tracker_abci.rounds import ( + SynchronizedData as PortfolioTrackerSyncedData, +) +from packages.valory.skills.strategy_evaluator_abci.payloads import IPFSHashPayload +from packages.valory.skills.trader_decision_maker_abci.rounds import ( + SynchronizedData as DecisionMakerSyncedData, +) +from packages.valory.skills.transaction_settlement_abci.rounds import ( + SynchronizedData as TxSettlementSyncedData, +) + + +class Event(Enum): + """Event enumeration for the price estimation demo.""" + + NO_ORDERS = "no_orders" + PREPARE_SWAP = "prepare_swap" + BACKTEST_POSITIVE_PROXY_SERVER = "prepare_swap_proxy_server" + BACKTEST_POSITIVE_EVM = "prepare_swap_evm" + PREPARE_INCOMPLETE_SWAP = "prepare_incomplete_swap" + ERROR_PREPARING_SWAPS = "error_preparing_swaps" + NO_INSTRUCTIONS = "no_instructions" + INSTRUCTIONS_PREPARED = "instructions_prepared" + TRANSACTION_PREPARED = "transaction_prepared" + INCOMPLETE_INSTRUCTIONS_PREPARED = "incomplete_instructions_prepared" + ERROR_PREPARING_INSTRUCTIONS = "error_preparing_instructions" + SWAP_TX_PREPARED = "swap_tx_prepared" + SWAPS_QUEUE_EMPTY = "swaps_queue_empty" + TX_PREPARATION_FAILED = "none" + PROXY_SWAPPED = "proxy_swapped" + PROXY_SWAP_FAILED = "proxy_swap_failed" + BACKTEST_POSITIVE = "backtest_succeeded" + BACKTEST_NEGATIVE = "backtest_negative" + BACKTEST_FAILED = "backtest_failed" + ERROR_BACKTESTING = "error_backtesting" + ROUND_TIMEOUT = "round_timeout" + PROXY_SWAP_TIMEOUT = "proxy_swap_timeout" + NO_MAJORITY = "no_majority" + + +class SynchronizedData( + DecisionMakerSyncedData, + MarketFetcherSyncedData, + PortfolioTrackerSyncedData, + TxSettlementSyncedData, +): + """Class to represent the synchronized data. + + This data is replicated by the tendermint application. + """ + + def _optional_str(self, db_key: str) -> Optional[str]: + """Get an optional string from the db.""" + val = self.db.get_strict(db_key) + if val is None: + return None + return str(val) + + def _get_deserialized(self, key: str) -> DeserializedCollection: + """Strictly get a collection and return it deserialized.""" + serialized = self.db.get_strict(key) + return CollectionRound.deserialize_collection(serialized) + + @property + def orders_hash(self) -> Optional[str]: + """Get the hash of the orders' data.""" + return self._optional_str("orders_hash") + + @property + def backtested_orders_hash(self) -> Optional[str]: + """Get the hash of the backtested orders' data.""" + return self._optional_str("backtested_orders_hash") + + @property + def incomplete_exec(self) -> bool: + """Get whether the strategies did not complete successfully.""" + return bool(self.db.get_strict("incomplete_exec")) + + @property + def tx_id(self) -> str: + """Get the transaction's id.""" + return str(self.db.get_strict("tx_id")) + + @property + def instructions_hash(self) -> Optional[str]: + """Get the hash of the instructions' data.""" + return self._optional_str("instructions_hash") + + @property + def incomplete_instructions(self) -> bool: + """Get whether the instructions were not built for all the swaps.""" + return bool(self.db.get_strict("incomplete_instructions")) + + @property + def participant_to_orders(self) -> DeserializedCollection: + """Get the participants to orders.""" + return self._get_deserialized("participant_to_orders") + + @property + def participant_to_instructions(self) -> DeserializedCollection: + """Get the participants to swap(s) instructions.""" + return self._get_deserialized("participant_to_instructions") + + @property + def participant_to_tx_preparation(self) -> DeserializedCollection: + """Get the participants to the next swap's tx preparation.""" + return self._get_deserialized("participant_to_tx_preparation") + + @property + def participant_to_backtesting(self) -> DeserializedCollection: + """Get the participants to the backtesting.""" + return self._get_deserialized("participant_to_backtesting") + + +class IPFSRound(CollectSameUntilThresholdRound): + """A round for sending data to IPFS and storing the returned hash.""" + + payload_class = IPFSHashPayload + synchronized_data_class = SynchronizedData + incomplete_event: Event + no_hash_event: Event + no_majority_event = Event.NO_MAJORITY + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: + """Process the end of the block.""" + res = super().end_block() + if res is None: + return None + + synced_data, event = cast(Tuple[SynchronizedData, Enum], res) + if event == self.done_event: + return synced_data, self.get_swap_event(synced_data) + return synced_data, event + + def get_swap_event(self, synced_data: SynchronizedData) -> Enum: + """Get the swap event based on the synchronized data.""" + if not isinstance(self.selection_key, tuple) or len(self.selection_key) != 2: + raise ValueError( + f"The default implementation of `get_swap_event` for {self.__class__!r} " + "only supports two selection keys. " + "Please override the method to match the intended logic." + ) + + hash_db_key, incomplete_db_key = self.selection_key + if getattr(synced_data, hash_db_key) is None: + return self.no_hash_event + if getattr(synced_data, incomplete_db_key): + return self.incomplete_event + return self.done_event diff --git a/packages/valory/skills/strategy_evaluator_abci/states/final_states.py b/packages/valory/skills/strategy_evaluator_abci/states/final_states.py new file mode 100644 index 0000000..dd34179 --- /dev/null +++ b/packages/valory/skills/strategy_evaluator_abci/states/final_states.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the final states of the strategy evaluator abci app.""" + +from packages.valory.skills.abstract_round_abci.base import DegenerateRound + + +class SwapTxPreparedRound(DegenerateRound): + """A round representing that the strategy evaluator has prepared swap(s) transaction.""" + + +class NoMoreSwapsRound(DegenerateRound): + """A round representing that the strategy evaluator has no more swap transactions to prepare.""" + + +class HodlRound(DegenerateRound): + """A round representing that the strategy evaluator has not prepared any swap transactions.""" + + +class StrategyExecutionFailedRound(DegenerateRound): + """A round representing that the strategy evaluator has failed to execute the strategy.""" + + +class InstructionPreparationFailedRound(DegenerateRound): + """A round representing that the strategy evaluator has failed to prepare the instructions for the swaps.""" + + +class BacktestingNegativeRound(DegenerateRound): + """A round representing that the backtesting has returned with a negative result.""" + + +class BacktestingFailedRound(DegenerateRound): + """A round representing that the backtesting has failed to run.""" + + +# TODO use this in portfolio tracker +# class RefillRequiredRound(DegenerateRound): +# """A round representing that a refill is required for swapping.""" diff --git a/packages/valory/skills/strategy_evaluator_abci/states/prepare_swap.py b/packages/valory/skills/strategy_evaluator_abci/states/prepare_swap.py new file mode 100644 index 0000000..18cd6ff --- /dev/null +++ b/packages/valory/skills/strategy_evaluator_abci/states/prepare_swap.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the swap(s) instructions' preparation state of the strategy evaluator abci app.""" + +from packages.valory.skills.abstract_round_abci.base import ( + CollectSameUntilThresholdRound, + get_name, +) +from packages.valory.skills.strategy_evaluator_abci.payloads import ( + TransactionHashPayload, +) +from packages.valory.skills.strategy_evaluator_abci.states.base import ( + Event, + IPFSRound, + SynchronizedData, +) + + +class PrepareSwapRound(IPFSRound): + """A round in which the agents prepare swap(s) instructions.""" + + done_event = Event.INSTRUCTIONS_PREPARED + incomplete_event = Event.INCOMPLETE_INSTRUCTIONS_PREPARED + no_hash_event = Event.NO_INSTRUCTIONS + none_event = Event.ERROR_PREPARING_INSTRUCTIONS + selection_key = ( + get_name(SynchronizedData.instructions_hash), + get_name(SynchronizedData.incomplete_instructions), + ) + collection_key = get_name(SynchronizedData.participant_to_instructions) + + +class PrepareEvmSwapRound(CollectSameUntilThresholdRound): + """A round in which agents compute the transaction hash.""" + + payload_class = TransactionHashPayload + synchronized_data_class = SynchronizedData + done_event = Event.TRANSACTION_PREPARED + none_event = Event.NO_INSTRUCTIONS + no_majority_event = Event.NO_MAJORITY + collection_key = get_name(SynchronizedData.participant_to_signature) + selection_key = get_name(SynchronizedData.most_voted_tx_hash) diff --git a/packages/valory/skills/strategy_evaluator_abci/states/proxy_swap_queue.py b/packages/valory/skills/strategy_evaluator_abci/states/proxy_swap_queue.py new file mode 100644 index 0000000..f7c049b --- /dev/null +++ b/packages/valory/skills/strategy_evaluator_abci/states/proxy_swap_queue.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the state for preparing a transaction for the next swap in the queue of instructions.""" + +from enum import Enum +from typing import Optional, Tuple, cast + +from packages.valory.skills.abstract_round_abci.base import ( + BaseSynchronizedData, + CollectSameUntilThresholdRound, + get_name, +) +from packages.valory.skills.strategy_evaluator_abci.payloads import SendSwapProxyPayload +from packages.valory.skills.strategy_evaluator_abci.states.base import ( + Event, + SynchronizedData, +) + + +class ProxySwapQueueRound(CollectSameUntilThresholdRound): + """A round in which one agent utilizes the proxy server to perform the next swap transaction in priority.""" + + payload_class = SendSwapProxyPayload + synchronized_data_class = SynchronizedData + done_event = Event.PROXY_SWAPPED + none_event = Event.PROXY_SWAP_FAILED + no_majority_event = Event.NO_MAJORITY + selection_key = get_name(SynchronizedData.tx_id) + collection_key = get_name(SynchronizedData.participant_to_tx_preparation) + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: + """Process the end of the block.""" + res = super().end_block() + if res is None: + return None + + synced_data, event = cast(Tuple[SynchronizedData, Enum], res) + if event == self.done_event and synced_data.tx_id == "": + return synced_data, Event.SWAPS_QUEUE_EMPTY + return synced_data, event diff --git a/packages/valory/skills/strategy_evaluator_abci/states/strategy_exec.py b/packages/valory/skills/strategy_evaluator_abci/states/strategy_exec.py new file mode 100644 index 0000000..ea8abd9 --- /dev/null +++ b/packages/valory/skills/strategy_evaluator_abci/states/strategy_exec.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the strategy execution state of the strategy evaluator abci app.""" + +from packages.valory.skills.abstract_round_abci.base import get_name +from packages.valory.skills.strategy_evaluator_abci.states.base import ( + Event, + IPFSRound, + SynchronizedData, +) + + +class StrategyExecRound(IPFSRound): + """A round for executing a strategy.""" + + done_event = Event.PREPARE_SWAP + incomplete_event = Event.PREPARE_INCOMPLETE_SWAP + no_hash_event = Event.NO_ORDERS + none_event = Event.ERROR_PREPARING_SWAPS + selection_key = ( + get_name(SynchronizedData.orders_hash), + get_name(SynchronizedData.incomplete_exec), + ) + collection_key = get_name(SynchronizedData.participant_to_orders) diff --git a/packages/valory/skills/strategy_evaluator_abci/states/swap_queue.py b/packages/valory/skills/strategy_evaluator_abci/states/swap_queue.py new file mode 100644 index 0000000..9657eb0 --- /dev/null +++ b/packages/valory/skills/strategy_evaluator_abci/states/swap_queue.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the state for preparing a transaction for the next swap in the queue of instructions.""" + +from enum import Enum +from typing import Optional, Tuple, cast + +from packages.valory.skills.abstract_round_abci.base import ( + BaseSynchronizedData, + CollectSameUntilThresholdRound, + get_name, +) +from packages.valory.skills.strategy_evaluator_abci.payloads import SendSwapPayload +from packages.valory.skills.strategy_evaluator_abci.states.base import ( + Event, + SynchronizedData, +) + + +class SwapQueueRound(CollectSameUntilThresholdRound): + """A round in which the agents prepare a swap transaction.""" + + payload_class = SendSwapPayload + synchronized_data_class = SynchronizedData + done_event = Event.SWAP_TX_PREPARED + none_event = Event.TX_PREPARATION_FAILED + no_majority_event = Event.NO_MAJORITY + # TODO replace `most_voted_randomness` with `most_voted_instruction_set` when solana tx settlement is ready + selection_key = get_name(SynchronizedData.most_voted_randomness) + collection_key = get_name(SynchronizedData.participant_to_tx_preparation) + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: + """Process the end of the block.""" + res = super().end_block() + if res is None: + return None + + synced_data, event = cast(Tuple[SynchronizedData, Enum], res) + # TODO replace `most_voted_randomness` with `most_voted_instruction_set` when solana tx settlement is ready + if event == self.done_event and synced_data.most_voted_randomness == "": + return synced_data, Event.SWAPS_QUEUE_EMPTY + return synced_data, event diff --git a/packages/valory/skills/trader_decision_maker_abci/README.md b/packages/valory/skills/trader_decision_maker_abci/README.md new file mode 100644 index 0000000..28bb394 --- /dev/null +++ b/packages/valory/skills/trader_decision_maker_abci/README.md @@ -0,0 +1,5 @@ +# TraderDecisionMakerAbci + +## Description + +This module contains the 'trader_decision_maker_abci' skill for an AEA. diff --git a/packages/valory/skills/trader_decision_maker_abci/__init__.py b/packages/valory/skills/trader_decision_maker_abci/__init__.py new file mode 100644 index 0000000..1808a23 --- /dev/null +++ b/packages/valory/skills/trader_decision_maker_abci/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains a strategy selection skill based on a greedy policy.""" + +from aea.configurations.base import PublicId + + +PUBLIC_ID = PublicId.from_str("valory/trader_decision_maker_abci:0.1.0") diff --git a/packages/valory/skills/trader_decision_maker_abci/behaviours.py b/packages/valory/skills/trader_decision_maker_abci/behaviours.py new file mode 100644 index 0000000..e5aa454 --- /dev/null +++ b/packages/valory/skills/trader_decision_maker_abci/behaviours.py @@ -0,0 +1,234 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the behaviours for the 'trader_decision_maker_abci' skill.""" + +import json +from abc import ABC +from pathlib import Path +from typing import ( + Any, + Callable, + Generator, + List, + Optional, + Set, + Tuple, + Type, + TypeVar, + cast, +) + +from packages.valory.skills.abstract_round_abci.base import AbstractRound +from packages.valory.skills.abstract_round_abci.behaviour_utils import BaseBehaviour +from packages.valory.skills.abstract_round_abci.behaviours import AbstractRoundBehaviour +from packages.valory.skills.abstract_round_abci.common import ( + RandomnessBehaviour as RandomnessBehaviourBase, +) +from packages.valory.skills.trader_decision_maker_abci.models import Params +from packages.valory.skills.trader_decision_maker_abci.payloads import ( + RandomnessPayload, + TraderDecisionMakerPayload, +) +from packages.valory.skills.trader_decision_maker_abci.policy import EGreedyPolicy +from packages.valory.skills.trader_decision_maker_abci.rounds import ( + Position, + RandomnessRound, + SynchronizedData, + TraderDecisionMakerAbciApp, + TraderDecisionMakerRound, +) + + +DeserializedType = TypeVar("DeserializedType") + + +POLICY_STORE = "policy_store.json" +POSITIONS_STORE = "positions.json" +STRATEGIES_STORE = "strategies.json" + + +class RandomnessBehaviour(RandomnessBehaviourBase): + """Retrieve randomness.""" + + matching_round = RandomnessRound + payload_class = RandomnessPayload + + +class TraderDecisionMakerBehaviour(BaseBehaviour, ABC): + """A behaviour in which the agents select a trading strategy.""" + + matching_round: Type[AbstractRound] = TraderDecisionMakerRound + + def __init__(self, **kwargs: Any) -> None: + """Initialize Behaviour.""" + super().__init__(**kwargs) + base_dir = Path(self.context.data_dir) + self.policy_path = base_dir / POLICY_STORE + self.positions_path = base_dir / POSITIONS_STORE + self.strategies_path = base_dir / STRATEGIES_STORE + self.strategies: Tuple[str, ...] = tuple(self.context.shared_state.keys()) + + @property + def params(self) -> Params: + """Get the parameters.""" + return cast(Params, self.context.params) + + @property + def synchronized_data(self) -> SynchronizedData: + """Return the synchronized data.""" + return SynchronizedData(super().synchronized_data.db) + + @property + def policy(self) -> EGreedyPolicy: + """Get the policy.""" + if self._policy is None: + raise ValueError( + "Attempting to retrieve the policy before it has been established." + ) + return self._policy + + @property + def is_first_period(self) -> bool: + """Return whether it is the first period of the service.""" + return self.synchronized_data.period_count == 0 + + @property + def positions(self) -> List[Position]: + """Get the positions of the service.""" + if self.is_first_period: + positions = self._try_recover_from_store( + self.positions_path, + Position.from_json, + ) + if positions is not None: + return positions + return [] + return self.synchronized_data.positions + + def _adjust_policy_strategies(self, local: List[str]) -> None: + """Add or remove strategies from the locally stored policy to match the strategies given via the config.""" + # remove strategies if they are not available anymore + # process the indices in a reverse order to avoid index shifting when removing the unavailable strategies later + reversed_idx = range(len(local) - 1, -1, -1) + removed_idx = [idx for idx in reversed_idx if local[idx] not in self.strategies] + self.policy.remove_strategies(removed_idx) + + # add strategies if there are new ones available + # process the indices in a reverse order to avoid index shifting when adding the new strategies later + reversed_idx = range(len(self.strategies) - 1, -1, -1) + new_idx = [idx for idx in reversed_idx if self.strategies[idx] not in local] + self.policy.add_new_strategies(new_idx) + + def _set_policy(self) -> None: + """Set the E Greedy Policy.""" + if not self.is_first_period: + self._policy = self.synchronized_data.policy + return + + self._policy = self._get_init_policy() + local_strategies = self._try_recover_from_store(self.strategies_path) + if local_strategies is not None: + self._adjust_policy_strategies(local_strategies) + + def _get_init_policy(self) -> EGreedyPolicy: + """Get the initial policy""" + # try to read the policy from the policy store + policy = self._try_recover_from_store( + self.policy_path, lambda policy_: EGreedyPolicy(**policy_) + ) + if policy is not None: + # we successfully recovered the policy, so we return it + return policy + + # we could not recover the policy, so we create a new one + n_relevant = len(self.strategies) + policy = EGreedyPolicy.initial_state(self.params.epsilon, n_relevant) + return policy + + def _try_recover_from_store( + self, + path: Path, + deserializer: Optional[Callable[[Any], DeserializedType]] = None, + ) -> Optional[DeserializedType]: + """Try to recover a previously saved file from the policy store.""" + try: + with open(path, "r") as stream: + res = json.load(stream) + if deserializer is None: + return res + return deserializer(res) + except Exception as e: + self.context.logger.warning( + f"Could not recover file from the policy store: {e}." + ) + return None + + def select_strategy(self) -> Optional[str]: + """Select a strategy based on an e-greedy policy and return its index.""" + self._set_policy() + randomness = self.synchronized_data.most_voted_randomness + selected_idx = self.policy.select_strategy(randomness) + selected = self.strategies[selected_idx] if selected_idx is not None else "NaN" + self.context.logger.info(f"Selected strategy: {selected}.") + return selected + + def _store_policy(self) -> None: + """Store the policy.""" + with open(self.policy_path, "w") as policy_stream: + policy_stream.write(self.policy.serialize()) + + def _store_available_strategies(self) -> None: + """Store the available strategies.""" + with open(self.strategies_path, "w") as strategies_stream: + json.dump(self.strategies, strategies_stream) + + def async_act(self) -> Generator: + """Do the action.""" + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + policy = positions = None + selected_strategy = self.select_strategy() + if selected_strategy is not None: + policy = self.policy.serialize() + positions = json.dumps(self.positions, sort_keys=True) + self._store_policy() + self._store_available_strategies() + + payload = TraderDecisionMakerPayload( + self.context.agent_address, + policy, + positions, + selected_strategy, + ) + + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + yield from self.send_a2a_transaction(payload) + yield from self.wait_until_round_end() + self.set_done() + + +class TraderDecisionMakerRoundBehaviour(AbstractRoundBehaviour): + """This behaviour manages the consensus stages for the TraderDecisionMakerBehaviour.""" + + initial_behaviour_cls = RandomnessBehaviour + abci_app_cls = TraderDecisionMakerAbciApp + behaviours: Set[Type[BaseBehaviour]] = { + RandomnessBehaviour, # type: ignore + TraderDecisionMakerBehaviour, # type: ignore + } diff --git a/packages/valory/skills/trader_decision_maker_abci/dialogues.py b/packages/valory/skills/trader_decision_maker_abci/dialogues.py new file mode 100644 index 0000000..661d747 --- /dev/null +++ b/packages/valory/skills/trader_decision_maker_abci/dialogues.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the classes required for dialogue management.""" + +from packages.valory.skills.abstract_round_abci.dialogues import ( + AbciDialogue as BaseAbciDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + AbciDialogues as BaseAbciDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + ContractApiDialogue as BaseContractApiDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + ContractApiDialogues as BaseContractApiDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + HttpDialogue as BaseHttpDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + HttpDialogues as BaseHttpDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + IpfsDialogue as BaseIpfsDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + IpfsDialogues as BaseIpfsDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + LedgerApiDialogue as BaseLedgerApiDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + LedgerApiDialogues as BaseLedgerApiDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + SigningDialogue as BaseSigningDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + SigningDialogues as BaseSigningDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + TendermintDialogue as BaseTendermintDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + TendermintDialogues as BaseTendermintDialogues, +) + + +AbciDialogue = BaseAbciDialogue +AbciDialogues = BaseAbciDialogues + + +HttpDialogue = BaseHttpDialogue +HttpDialogues = BaseHttpDialogues + + +SigningDialogue = BaseSigningDialogue +SigningDialogues = BaseSigningDialogues + + +LedgerApiDialogue = BaseLedgerApiDialogue +LedgerApiDialogues = BaseLedgerApiDialogues + + +ContractApiDialogue = BaseContractApiDialogue +ContractApiDialogues = BaseContractApiDialogues + +TendermintDialogue = BaseTendermintDialogue +TendermintDialogues = BaseTendermintDialogues + + +IpfsDialogue = BaseIpfsDialogue +IpfsDialogues = BaseIpfsDialogues diff --git a/packages/valory/skills/trader_decision_maker_abci/fsm_specification.yaml b/packages/valory/skills/trader_decision_maker_abci/fsm_specification.yaml new file mode 100644 index 0000000..ff06e9a --- /dev/null +++ b/packages/valory/skills/trader_decision_maker_abci/fsm_specification.yaml @@ -0,0 +1,25 @@ +alphabet_in: +- DONE +- NONE +- NO_MAJORITY +- ROUND_TIMEOUT +default_start_state: RandomnessRound +final_states: +- FailedTraderDecisionMakerRound +- FinishedTraderDecisionMakerRound +label: TraderDecisionMakerAbciApp +start_states: +- RandomnessRound +states: +- FailedTraderDecisionMakerRound +- FinishedTraderDecisionMakerRound +- RandomnessRound +- TraderDecisionMakerRound +transition_func: + (RandomnessRound, DONE): TraderDecisionMakerRound + (RandomnessRound, NO_MAJORITY): RandomnessRound + (RandomnessRound, ROUND_TIMEOUT): RandomnessRound + (TraderDecisionMakerRound, DONE): FinishedTraderDecisionMakerRound + (TraderDecisionMakerRound, NONE): FailedTraderDecisionMakerRound + (TraderDecisionMakerRound, NO_MAJORITY): FailedTraderDecisionMakerRound + (TraderDecisionMakerRound, ROUND_TIMEOUT): FailedTraderDecisionMakerRound diff --git a/packages/valory/skills/trader_decision_maker_abci/handlers.py b/packages/valory/skills/trader_decision_maker_abci/handlers.py new file mode 100644 index 0000000..2b18af0 --- /dev/null +++ b/packages/valory/skills/trader_decision_maker_abci/handlers.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + + +"""This module contains the handlers for the 'trader_decision_maker_abci' skill.""" + +from packages.valory.skills.abstract_round_abci.handlers import ABCIRoundHandler +from packages.valory.skills.abstract_round_abci.handlers import ( + ContractApiHandler as BaseContractApiHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + HttpHandler as BaseHttpHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + IpfsHandler as BaseIpfsHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + LedgerApiHandler as BaseLedgerApiHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + SigningHandler as BaseSigningHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + TendermintHandler as BaseTendermintHandler, +) + + +ABCITraderDecisionMakerHandler = ABCIRoundHandler +HttpHandler = BaseHttpHandler +SigningHandler = BaseSigningHandler +LedgerApiHandler = BaseLedgerApiHandler +ContractApiHandler = BaseContractApiHandler +TendermintHandler = BaseTendermintHandler +IpfsHandler = BaseIpfsHandler diff --git a/packages/valory/skills/trader_decision_maker_abci/models.py b/packages/valory/skills/trader_decision_maker_abci/models.py new file mode 100644 index 0000000..811a336 --- /dev/null +++ b/packages/valory/skills/trader_decision_maker_abci/models.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + + +"""Custom objects for the 'trader_decision_maker_abci' skill.""" + +from typing import Any + +from packages.valory.skills.abstract_round_abci.models import BaseParams +from packages.valory.skills.abstract_round_abci.models import ( + BenchmarkTool as BaseBenchmarkTool, +) +from packages.valory.skills.abstract_round_abci.models import Requests as BaseRequests +from packages.valory.skills.abstract_round_abci.models import ( + SharedState as BaseSharedState, +) +from packages.valory.skills.trader_decision_maker_abci.rounds import ( + TraderDecisionMakerAbciApp, +) + + +Requests = BaseRequests +BenchmarkTool = BaseBenchmarkTool + + +class SharedState(BaseSharedState): + """Keep the current shared state of the skill.""" + + abci_app_cls = TraderDecisionMakerAbciApp + + +class Params(BaseParams): + """Market manager's parameters.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize the parameters' object.""" + self.epsilon: float = self._ensure("epsilon", kwargs, float) + super().__init__(*args, **kwargs) diff --git a/packages/valory/skills/trader_decision_maker_abci/payloads.py b/packages/valory/skills/trader_decision_maker_abci/payloads.py new file mode 100644 index 0000000..499d308 --- /dev/null +++ b/packages/valory/skills/trader_decision_maker_abci/payloads.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the transaction payloads for the 'trader_decision_maker_abci' skill.""" + +from dataclasses import dataclass +from typing import Optional + +from packages.valory.skills.abstract_round_abci.base import BaseTxPayload + + +@dataclass(frozen=True) +class RandomnessPayload(BaseTxPayload): + """Represent a transaction payload carrying randomness data.""" + + round_id: int + randomness: str + + +@dataclass(frozen=True) +class TraderDecisionMakerPayload(BaseTxPayload): + """A transaction payload for the TraderDecisionMakingRound.""" + + policy: Optional[str] + positions: Optional[str] + selected_strategy: Optional[str] diff --git a/packages/valory/skills/trader_decision_maker_abci/policy.py b/packages/valory/skills/trader_decision_maker_abci/policy.py new file mode 100644 index 0000000..361031e --- /dev/null +++ b/packages/valory/skills/trader_decision_maker_abci/policy.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2023-2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains an Epsilon Greedy Policy implementation.""" + +import json +import random +from dataclasses import dataclass +from typing import List, Optional, Union + +from packages.valory.skills.trader_decision_maker_abci.utils import DataclassEncoder + + +RandomnessType = Union[int, float, str, bytes, bytearray, None] + + +def argmax(li: List) -> int: + """Get the index of the max value within the provided list.""" + return li.index((max(li))) + + +@dataclass +class EGreedyPolicy: + """An e-Greedy policy for the strategy selection.""" + + eps: float + counts: List[int] + rewards: List[float] + initial_value = 0 + + @classmethod + def initial_state(cls, eps: float, n_strategies: int) -> "EGreedyPolicy": + """Return an instance on its initial state.""" + if n_strategies <= 0 or eps > 1 or eps < 0: + raise ValueError( + f"Cannot initialize an e Greedy Policy with {eps=} and {n_strategies=}" + ) + + return EGreedyPolicy( + eps, + [cls.initial_value] * n_strategies, + [float(cls.initial_value)] * n_strategies, + ) + + @property + def n_strategies(self) -> int: + """Get the number of the policy's strategies.""" + return len(self.counts) + + @property + def random_strategy(self) -> int: + """Get the index of a strategy randomly.""" + return random.randrange(self.n_strategies) # nosec + + @property + def has_updated(self) -> bool: + """Whether the policy has ever been updated since its genesis or not.""" + return sum(self.counts) > 0 + + @property + def reward_rates(self) -> List[float]: + """Get the reward rates.""" + return [ + reward / count if count > 0 else 0 + for reward, count in zip(self.rewards, self.counts) + ] + + @property + def best_strategy(self) -> int: + """Get the best strategy.""" + return argmax(self.reward_rates) + + def add_new_strategies(self, indexes: List[int], avoid_shift: bool = False) -> None: + """Add new strategies to the current policy.""" + if avoid_shift: + indexes = sorted(indexes, reverse=True) + + for i in indexes: + self.counts.insert(i, self.initial_value) + self.rewards.insert(i, float(self.initial_value)) + + def remove_strategies(self, indexes: List[int], avoid_shift: bool = False) -> None: + """Remove the knowledge for the strategies corresponding to the given indexes.""" + if avoid_shift: + indexes = sorted(indexes, reverse=True) + + for i in indexes: + try: + del self.counts[i] + del self.rewards[i] + except IndexError as exc: + error = "Attempted to remove strategies using incorrect indexes!" + raise ValueError(error) from exc + + def select_strategy(self, randomness: RandomnessType) -> Optional[int]: + """Select a strategy and return its index.""" + if self.n_strategies == 0: + return None + + random.seed(randomness) + if sum(self.reward_rates) == 0 or random.random() < self.eps: # nosec + return self.random_strategy + + return self.best_strategy + + def add_reward(self, index: int, reward: float = 0) -> None: + """Add a reward for the strategy corresponding to the given index.""" + self.counts[index] += 1 + self.rewards[index] += reward + + def serialize(self) -> str: + """Return the policy serialized.""" + return json.dumps(self, cls=DataclassEncoder, sort_keys=True) diff --git a/packages/valory/skills/trader_decision_maker_abci/rounds.py b/packages/valory/skills/trader_decision_maker_abci/rounds.py new file mode 100644 index 0000000..c5bc79e --- /dev/null +++ b/packages/valory/skills/trader_decision_maker_abci/rounds.py @@ -0,0 +1,232 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the rounds for the 'trader_decision_maker_abci' skill.""" + +import json +from abc import ABC +from dataclasses import dataclass +from enum import Enum +from typing import Dict, List, Set, Tuple, Type, cast + +from packages.valory.skills.abstract_round_abci.base import ( + AbciApp, + AbciAppTransitionFunction, + AbstractRound, + AppState, + BaseSynchronizedData, + CollectSameUntilThresholdRound, + CollectionRound, + DegenerateRound, + DeserializedCollection, + get_name, +) +from packages.valory.skills.trader_decision_maker_abci.payloads import ( + RandomnessPayload, + TraderDecisionMakerPayload, +) +from packages.valory.skills.trader_decision_maker_abci.policy import EGreedyPolicy + + +class Event(Enum): + """Event enumeration for the TraderDecisionMakerAbci demo.""" + + DONE = "done" + NONE = "none" + NO_MAJORITY = "no_majority" + ROUND_TIMEOUT = "round_timeout" + + +@dataclass +class Position: + """A swap position.""" + + from_token: str + to_token: str + amount: int + + @classmethod + def from_json(cls, positions: List[Dict]) -> List["Position"]: + """Return a list of positions from a JSON representation.""" + return [cls(**position) for position in positions] + + +class SynchronizedData(BaseSynchronizedData): + """Class to represent the synchronized data. + + This data is replicated by the tendermint application. + """ + + def _get_deserialized(self, key: str) -> DeserializedCollection: + """Strictly get a collection and return it deserialized.""" + serialized = self.db.get_strict(key) + return CollectionRound.deserialize_collection(serialized) + + @property + def participant_to_decision(self) -> DeserializedCollection: + """Get the participants to decision.""" + return self._get_deserialized("participant_to_decision") + + @property + def most_voted_randomness_round(self) -> int: + """Get the most voted randomness round.""" + round_ = self.db.get_strict("most_voted_randomness_round") + return int(round_) + + @property + def selected_strategy(self) -> str: + """Get the selected strategy.""" + return str(self.db.get_strict("selected_strategy")) + + @property + def policy(self) -> EGreedyPolicy: + """Get the policy.""" + policy = self.db.get_strict("policy") + return EGreedyPolicy(**json.loads(policy)) + + @property + def positions(self) -> List[Position]: + """Get the swap positions.""" + positions = json.loads(self.db.get_strict("positions")) + return Position.from_json(positions) + + +class TraderDecisionMakerAbstractRound(AbstractRound[Event], ABC): + """Abstract round for the TraderDecisionMakerAbci skill.""" + + @property + def synchronized_data(self) -> SynchronizedData: + """Return the synchronized data.""" + return cast(SynchronizedData, super().synchronized_data) + + def _return_no_majority_event(self) -> Tuple[SynchronizedData, Event]: + """ + Trigger the `NO_MAJORITY` event. + + :return: the new synchronized data and a `NO_MAJORITY` event + """ + return self.synchronized_data, Event.NO_MAJORITY + + +class RandomnessRound(CollectSameUntilThresholdRound): + """A round for generating randomness.""" + + payload_class = RandomnessPayload + synchronized_data_class = SynchronizedData + done_event = Event.DONE + no_majority_event = Event.NO_MAJORITY + collection_key = get_name(SynchronizedData.participant_to_randomness) + selection_key = ( + get_name(SynchronizedData.most_voted_randomness_round), + get_name(SynchronizedData.most_voted_randomness), + ) + + +class TraderDecisionMakerRound( + CollectSameUntilThresholdRound, TraderDecisionMakerAbstractRound +): + """A round for the bets fetching & updating.""" + + payload_class = TraderDecisionMakerPayload + done_event: Enum = Event.DONE + none_event: Enum = Event.NONE + no_majority_event: Enum = Event.NO_MAJORITY + selection_key = ( + get_name(SynchronizedData.policy), + get_name(SynchronizedData.positions), + get_name(SynchronizedData.selected_strategy), + ) + collection_key = get_name(SynchronizedData.participant_to_decision) + synchronized_data_class = SynchronizedData + + +class FinishedTraderDecisionMakerRound(DegenerateRound, ABC): + """A round that represents that the ABCI app has finished""" + + +class FailedTraderDecisionMakerRound(DegenerateRound, ABC): + """A round that represents that the ABCI app has failed""" + + +class TraderDecisionMakerAbciApp(AbciApp[Event]): + """TraderDecisionMakerAbciApp + + Initial round: RandomnessRound + + Initial states: {RandomnessRound} + + Transition states: + 0. RandomnessRound + - done: 1. + - round timeout: 0. + - no majority: 0. + 1. TraderDecisionMakerRound + - done: 2. + - none: 3. + - round timeout: 3. + - no majority: 3. + 2. FinishedTraderDecisionMakerRound + 3. FailedTraderDecisionMakerRound + + Final states: {FailedTraderDecisionMakerRound, FinishedTraderDecisionMakerRound} + + Timeouts: + round timeout: 30.0 + """ + + initial_round_cls: Type[AbstractRound] = RandomnessRound + transition_function: AbciAppTransitionFunction = { + RandomnessRound: { + Event.DONE: TraderDecisionMakerRound, + Event.ROUND_TIMEOUT: RandomnessRound, + Event.NO_MAJORITY: RandomnessRound, + }, + TraderDecisionMakerRound: { + Event.DONE: FinishedTraderDecisionMakerRound, + Event.NONE: FailedTraderDecisionMakerRound, + Event.ROUND_TIMEOUT: FailedTraderDecisionMakerRound, + Event.NO_MAJORITY: FailedTraderDecisionMakerRound, + }, + FinishedTraderDecisionMakerRound: {}, + FailedTraderDecisionMakerRound: {}, + } + final_states: Set[AppState] = { + FinishedTraderDecisionMakerRound, + FailedTraderDecisionMakerRound, + } + event_to_timeout: Dict[Event, float] = { + Event.ROUND_TIMEOUT: 30.0, + } + cross_period_persisted_keys = frozenset( + {get_name(SynchronizedData.policy), get_name(SynchronizedData.positions)} + ) + db_pre_conditions: Dict[AppState, Set[str]] = {RandomnessRound: set()} + db_post_conditions: Dict[AppState, Set[str]] = { + FinishedTraderDecisionMakerRound: { + get_name(SynchronizedData.selected_strategy), + get_name(SynchronizedData.policy), + get_name(SynchronizedData.positions), + get_name(SynchronizedData.most_voted_randomness_round), + get_name(SynchronizedData.most_voted_randomness), + }, + FailedTraderDecisionMakerRound: { + get_name(SynchronizedData.most_voted_randomness_round), + get_name(SynchronizedData.most_voted_randomness), + }, + } diff --git a/packages/valory/skills/trader_decision_maker_abci/skill.yaml b/packages/valory/skills/trader_decision_maker_abci/skill.yaml new file mode 100644 index 0000000..0dc7ace --- /dev/null +++ b/packages/valory/skills/trader_decision_maker_abci/skill.yaml @@ -0,0 +1,142 @@ +name: trader_decision_maker_abci +author: valory +version: 0.1.0 +type: skill +description: This skill implements the TraderDecisionMakerAbci for an AEA. +license: Apache-2.0 +aea_version: '>=1.0.0, <2.0.0' +fingerprint: + README.md: bafybeih6idgiwf3bbqhikxeldldnhqtrkyi2gf4lfctrkivdy5pno6ilte + __init__.py: bafybeie5vho3br34gvl6x2lqfnudpjjeqgcfguvxl3p7l4kiyfxe5e323u + behaviours.py: bafybeih2i54x7jikjdxfdekpfylwcs4fjiz46ylynlzathpt5jl2xz6p4a + dialogues.py: bafybeieyljhi2bfvzytt6kxstbec7g5h5cauaul76guloysriqm22yo5fm + fsm_specification.yaml: bafybeihlwtlx3k2dcbjmahesfppgfrkruwwi75lcji6meqeobyaxjnydqu + handlers.py: bafybeifke4t4gg6fqb3zy3c6t4yginstypknvfgvj5u67yibhxlmo2cguy + models.py: bafybeifkzeulrztqnyekqrugcwchy4os4yz75rkxnxgmnmjnxp3qiiyxny + payloads.py: bafybeicwjheax7c4wpjcaxysbhwxoxv3z4gxldbcilv4gbhwshxahmjkru + policy.py: bafybeie5xilj3gv3yo3licei4r7hlfli65n4curxe3savtoj6unzksngtq + rounds.py: bafybeid3isvqi7oune2qrz2vj55ydew6ifaf5pnktp4m7xhy6ib73luo54 + utils.py: bafybeiblton2rvdxlv3bnu3asubtfwt2tk77lwrxw3hmesv3qarh3ew6ke +fingerprint_ignore_patterns: [] +connections: [] +contracts: [] +protocols: [] +skills: +- valory/abstract_round_abci:0.1.0:bafybeibovsktd3uxur45nrcomq5shcn46cgxd5idmhxbmjhg32c5abyqim +behaviours: + main: + args: {} + class_name: TraderDecisionMakerBehaviour +handlers: + abci: + args: {} + class_name: ABCITraderDecisionMakerHandler + contract_api: + args: {} + class_name: ContractApiHandler + http: + args: {} + class_name: HttpHandler + ipfs: + args: {} + class_name: IpfsHandler + ledger_api: + args: {} + class_name: LedgerApiHandler + signing: + args: {} + class_name: SigningHandler + tendermint: + args: {} + class_name: TendermintHandler +models: + abci_dialogues: + args: {} + class_name: AbciDialogues + benchmark_tool: + args: + log_dir: /logs + class_name: BenchmarkTool + contract_api_dialogues: + args: {} + class_name: ContractApiDialogues + http_dialogues: + args: {} + class_name: HttpDialogues + ipfs_dialogues: + args: {} + class_name: IpfsDialogues + ledger_api_dialogues: + args: {} + class_name: LedgerApiDialogues + params: + args: + cleanup_history_depth: 1 + cleanup_history_depth_current: null + drand_public_key: 868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31 + genesis_config: + genesis_time: '2022-05-20T16:00:21.735122717Z' + chain_id: chain-c4daS1 + consensus_params: + block: + max_bytes: '22020096' + max_gas: '-1' + time_iota_ms: '1000' + evidence: + max_age_num_blocks: '100000' + max_age_duration: '172800000000000' + max_bytes: '1048576' + validator: + pub_key_types: + - ed25519 + version: {} + voting_power: '10' + keeper_timeout: 30.0 + max_attempts: 10 + max_healthcheck: 120 + multisend_address: '0x0000000000000000000000000000000000000000' + on_chain_service_id: null + request_retry_delay: 1.0 + request_timeout: 10.0 + reset_pause_duration: 10 + reset_tendermint_after: 2 + retry_attempts: 400 + retry_timeout: 3 + round_timeout_seconds: 350.0 + service_id: market_manager + service_registry_address: null + setup: + all_participants: + - '0x0000000000000000000000000000000000000000' + safe_contract_address: '0x0000000000000000000000000000000000000000' + consensus_threshold: null + share_tm_config_on_startup: false + sleep_time: 5 + tendermint_check_sleep_delay: 3 + tendermint_com_url: http://localhost:8080 + tendermint_max_retries: 5 + tendermint_p2p_url: localhost:26656 + tendermint_url: http://localhost:26657 + termination_sleep: 900 + tx_timeout: 10.0 + use_termination: false + use_slashing: false + slash_cooldown_hours: 3 + slash_threshold_amount: 10000000000000000 + light_slash_unit_amount: 5000000000000000 + serious_slash_unit_amount: 8000000000000000 + epsilon: 0.1 + class_name: TraderDecisionMakerParams + requests: + args: {} + class_name: Requests + signing_dialogues: + args: {} + class_name: SigningDialogues + state: + args: {} + class_name: SharedState +dependencies: + web3: + version: <7,>=6.0.0 +is_abstract: true diff --git a/packages/valory/skills/trader_decision_maker_abci/utils.py b/packages/valory/skills/trader_decision_maker_abci/utils.py new file mode 100644 index 0000000..446d464 --- /dev/null +++ b/packages/valory/skills/trader_decision_maker_abci/utils.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains general purpose utilities.""" + +import json +from dataclasses import asdict, is_dataclass +from typing import Any + + +class DataclassEncoder(json.JSONEncoder): + """A custom JSON encoder for dataclasses.""" + + def default(self, o: Any) -> Any: + """The default JSON encoder.""" + if is_dataclass(o): + return asdict(o) + return super().default(o) diff --git a/packages/valory/skills/transaction_settlement_abci/README.md b/packages/valory/skills/transaction_settlement_abci/README.md new file mode 100644 index 0000000..29124a9 --- /dev/null +++ b/packages/valory/skills/transaction_settlement_abci/README.md @@ -0,0 +1,53 @@ +# Transaction settlement abci + +## Description + +This module contains the ABCI transaction settlement skill for an AEA. + +## Behaviours + +* `BaseResetBehaviour` + + Reset state. + +* `FinalizeBehaviour` + + Finalize state. + +* `RandomnessTransactionSubmissionBehaviour` + + Retrieve randomness. + +* `ResetAndPauseBehaviour` + + Reset and pause state. + +* `ResetBehaviour` + + Reset state. + +* `SelectKeeperTransactionSubmissionBehaviourA` + + Select the keeper agent. + +* `SelectKeeperTransactionSubmissionBehaviourB` + + Select the keeper agent. + +* `SignatureBehaviour` + + Signature state. + +* `TransactionSettlementBaseState` + + Base state behaviour for the common apps' skill. + +* `ValidateTransactionBehaviour` + + Validate a transaction. + + +## Handlers + +No Handlers (the skill is purely behavioural). + diff --git a/packages/valory/skills/transaction_settlement_abci/__init__.py b/packages/valory/skills/transaction_settlement_abci/__init__.py new file mode 100644 index 0000000..1df75f1 --- /dev/null +++ b/packages/valory/skills/transaction_settlement_abci/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the ABCI transaction settlement skill for an AEA.""" + +from aea.configurations.base import PublicId + + +PUBLIC_ID = PublicId.from_str("valory/transaction_settlement_abci:0.1.0") diff --git a/packages/valory/skills/transaction_settlement_abci/behaviours.py b/packages/valory/skills/transaction_settlement_abci/behaviours.py new file mode 100644 index 0000000..d3a7ca2 --- /dev/null +++ b/packages/valory/skills/transaction_settlement_abci/behaviours.py @@ -0,0 +1,984 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the behaviours for the 'abci' skill.""" + +import binascii +import pprint +import re +from abc import ABC +from collections import deque +from typing import ( + Any, + Deque, + Dict, + Generator, + Iterator, + List, + Optional, + Set, + Tuple, + Type, + Union, + cast, +) + +from aea.protocols.base import Message +from web3.types import Nonce, TxData, Wei + +from packages.valory.contracts.gnosis_safe.contract import GnosisSafeContract +from packages.valory.protocols.contract_api.message import ContractApiMessage +from packages.valory.skills.abstract_round_abci.behaviour_utils import RPCResponseStatus +from packages.valory.skills.abstract_round_abci.behaviours import ( + AbstractRoundBehaviour, + BaseBehaviour, +) +from packages.valory.skills.abstract_round_abci.common import ( + RandomnessBehaviour, + SelectKeeperBehaviour, +) +from packages.valory.skills.abstract_round_abci.utils import VerifyDrand +from packages.valory.skills.transaction_settlement_abci.models import TransactionParams +from packages.valory.skills.transaction_settlement_abci.payload_tools import ( + VerificationStatus, + skill_input_hex_to_payload, + tx_hist_payload_to_hex, +) +from packages.valory.skills.transaction_settlement_abci.payloads import ( + CheckTransactionHistoryPayload, + FinalizationTxPayload, + RandomnessPayload, + ResetPayload, + SelectKeeperPayload, + SignaturePayload, + SynchronizeLateMessagesPayload, + ValidatePayload, +) +from packages.valory.skills.transaction_settlement_abci.rounds import ( + CheckLateTxHashesRound, + CheckTransactionHistoryRound, + CollectSignatureRound, + FinalizationRound, + RandomnessTransactionSubmissionRound, + ResetRound, + SelectKeeperTransactionSubmissionARound, + SelectKeeperTransactionSubmissionBAfterTimeoutRound, + SelectKeeperTransactionSubmissionBRound, + SynchronizeLateMessagesRound, + SynchronizedData, + TransactionSubmissionAbciApp, + ValidateTransactionRound, +) + + +TxDataType = Dict[str, Union[VerificationStatus, Deque[str], int, Set[str], str]] + +drand_check = VerifyDrand() + +REVERT_CODE_RE = r"\s(GS\d{3})[^\d]" + +# This mapping was copied from: +# https://github.com/safe-global/safe-contracts/blob/ce5cbd256bf7a8a34538c7e5f1f2366a9d685f34/docs/error_codes.md +REVERT_CODES_TO_REASONS: Dict[str, str] = { + "GS000": "Could not finish initialization", + "GS001": "Threshold needs to be defined", + "GS010": "Not enough gas to execute Safe transaction", + "GS011": "Could not pay gas costs with ether", + "GS012": "Could not pay gas costs with token", + "GS013": "Safe transaction failed when gasPrice and safeTxGas were 0", + "GS020": "Signatures data too short", + "GS021": "Invalid contract signature location: inside static part", + "GS022": "Invalid contract signature location: length not present", + "GS023": "Invalid contract signature location: data not complete", + "GS024": "Invalid contract signature provided", + "GS025": "Hash has not been approved", + "GS026": "Invalid owner provided", + "GS030": "Only owners can approve a hash", + "GS031": "Method can only be called from this contract", + "GS100": "Modules have already been initialized", + "GS101": "Invalid module address provided", + "GS102": "Module has already been added", + "GS103": "Invalid prevModule, module pair provided", + "GS104": "Method can only be called from an enabled module", + "GS200": "Owners have already been setup", + "GS201": "Threshold cannot exceed owner count", + "GS202": "Threshold needs to be greater than 0", + "GS203": "Invalid owner address provided", + "GS204": "Address is already an owner", + "GS205": "Invalid prevOwner, owner pair provided", + "GS300": "Guard does not implement IERC165", +} + + +class TransactionSettlementBaseBehaviour(BaseBehaviour, ABC): + """Base behaviour for the common apps' skill.""" + + @property + def synchronized_data(self) -> SynchronizedData: + """Return the synchronized data.""" + return cast(SynchronizedData, super().synchronized_data) + + @property + def params(self) -> TransactionParams: + """Return the params.""" + return cast(TransactionParams, super().params) + + @staticmethod + def serialized_keepers(keepers: Deque[str], keeper_retries: int) -> str: + """Get the keepers serialized.""" + if len(keepers) == 0: + return "" + keepers_ = "".join(keepers) + keeper_retries_ = keeper_retries.to_bytes(32, "big").hex() + concatenated = keeper_retries_ + keepers_ + + return concatenated + + def get_gas_price_params(self, tx_body: dict) -> List[str]: + """Guess the gas strategy from the transaction params""" + strategy_to_params: Dict[str, List[str]] = { + "eip": ["maxPriorityFeePerGas", "maxFeePerGas"], + "gas_station": ["gasPrice"], + } + + for strategy, params in strategy_to_params.items(): + if all(param in tx_body for param in params): + self.context.logger.info(f"Detected gas strategy: {strategy}") + return params + + return [] + + def _get_tx_data( + self, + message: ContractApiMessage, + use_flashbots: bool, + manual_gas_limit: int = 0, + raise_on_failed_simulation: bool = False, + chain_id: Optional[str] = None, + ) -> Generator[None, None, TxDataType]: + """Get the transaction data from a `ContractApiMessage`.""" + tx_data: TxDataType = { + "status": VerificationStatus.PENDING, + "keepers": self.synchronized_data.keepers, + "keeper_retries": self.synchronized_data.keeper_retries, + "blacklisted_keepers": self.synchronized_data.blacklisted_keepers, + "tx_digest": "", + } + + # Check for errors in the transaction preparation + if ( + message.performative == ContractApiMessage.Performative.ERROR + and message.message is not None + ): + if self._safe_nonce_reused(message.message): + tx_data["status"] = VerificationStatus.VERIFIED + else: + tx_data["status"] = VerificationStatus.ERROR + self.context.logger.warning(self._parse_revert_reason(message)) + return tx_data + + # Check that we have a RAW_TRANSACTION response + if message.performative != ContractApiMessage.Performative.RAW_TRANSACTION: + self.context.logger.warning( + f"get_raw_safe_transaction unsuccessful! Received: {message}" + ) + return tx_data + + if manual_gas_limit > 0: + message.raw_transaction.body["gas"] = manual_gas_limit + + # Send transaction + tx_digest, rpc_status = yield from self.send_raw_transaction( + message.raw_transaction, + use_flashbots, + raise_on_failed_simulation=raise_on_failed_simulation, + chain_id=chain_id, + ) + + # Handle transaction results + if rpc_status == RPCResponseStatus.ALREADY_KNOWN: + self.context.logger.warning( + "send_raw_transaction unsuccessful! Transaction is already in the mempool! Will attempt to verify it." + ) + + if rpc_status == RPCResponseStatus.INCORRECT_NONCE: + tx_data["status"] = VerificationStatus.ERROR + self.context.logger.warning( + "send_raw_transaction unsuccessful! Incorrect nonce." + ) + + if rpc_status == RPCResponseStatus.INSUFFICIENT_FUNDS: + # blacklist self. + tx_data["status"] = VerificationStatus.INSUFFICIENT_FUNDS + blacklisted = cast(Deque[str], tx_data["keepers"]).popleft() + tx_data["keeper_retries"] = 1 + cast(Set[str], tx_data["blacklisted_keepers"]).add(blacklisted) + self.context.logger.warning( + "send_raw_transaction unsuccessful! Insufficient funds." + ) + + if rpc_status not in { + RPCResponseStatus.SUCCESS, + RPCResponseStatus.ALREADY_KNOWN, + }: + self.context.logger.warning( + f"send_raw_transaction unsuccessful! Received: {rpc_status}" + ) + return tx_data + + tx_data["tx_digest"] = cast(str, tx_digest) + + nonce = Nonce(int(cast(str, message.raw_transaction.body["nonce"]))) + fallback_gas = message.raw_transaction.body["gas"] + + # Get the gas params + gas_price_params = self.get_gas_price_params(message.raw_transaction.body) + + gas_price = { + gas_price_param: Wei( + int( + cast( + str, + message.raw_transaction.body[gas_price_param], + ) + ) + ) + for gas_price_param in gas_price_params + } + + # Set hash, nonce and tip. + self.params.mutable_params.tx_hash = cast(str, tx_data["tx_digest"]) + if nonce == self.params.mutable_params.nonce: + self.context.logger.info( + "Attempting to replace transaction " + f"with old gas price parameters {self.params.mutable_params.gas_price}, using new gas price parameters {gas_price}" + ) + else: + self.context.logger.info( + f"Sent transaction for mining with gas parameters {gas_price}" + ) + self.params.mutable_params.nonce = nonce + self.params.mutable_params.gas_price = gas_price + self.params.mutable_params.fallback_gas = fallback_gas + + return tx_data + + def _verify_tx(self, tx_hash: str) -> Generator[None, None, ContractApiMessage]: + """Verify a transaction.""" + tx_params = skill_input_hex_to_payload( + self.synchronized_data.most_voted_tx_hash + ) + chain_id = self.synchronized_data.get_chain_id(self.params.default_chain_id) + contract_api_msg = yield from self.get_contract_api_response( + performative=ContractApiMessage.Performative.GET_STATE, # type: ignore + contract_address=self.synchronized_data.safe_contract_address, + contract_id=str(GnosisSafeContract.contract_id), + contract_callable="verify_tx", + tx_hash=tx_hash, + owners=tuple(self.synchronized_data.participants), + to_address=tx_params["to_address"], + value=tx_params["ether_value"], + data=tx_params["data"], + safe_tx_gas=tx_params["safe_tx_gas"], + signatures_by_owner={ + key: payload.signature + for key, payload in self.synchronized_data.participant_to_signature.items() + }, + operation=tx_params["operation"], + chain_id=chain_id, + ) + return contract_api_msg + + @staticmethod + def _safe_nonce_reused(revert_reason: str) -> bool: + """Check for GS026.""" + return "GS026" in revert_reason + + @staticmethod + def _parse_revert_reason(message: ContractApiMessage) -> str: + """Parse a revert reason and log a relevant message.""" + default_message = f"get_raw_safe_transaction unsuccessful! Received: {message}" + + revert_reason = message.message + if not revert_reason: + return default_message + + revert_match = re.findall(REVERT_CODE_RE, revert_reason) + if revert_match is None or len(revert_match) != 1: + return default_message + + revert_code = revert_match.pop() + revert_explanation = REVERT_CODES_TO_REASONS.get(revert_code, None) + if revert_explanation is None: + return default_message + + return f"Received a {revert_code} revert error: {revert_explanation}." + + def _get_safe_nonce(self) -> Generator[None, None, ContractApiMessage]: + """Get the safe nonce.""" + chain_id = self.synchronized_data.get_chain_id(self.params.default_chain_id) + contract_api_msg = yield from self.get_contract_api_response( + performative=ContractApiMessage.Performative.GET_STATE, # type: ignore + contract_address=self.synchronized_data.safe_contract_address, + contract_id=str(GnosisSafeContract.contract_id), + contract_callable="get_safe_nonce", + chain_id=chain_id, + ) + return contract_api_msg + + +class RandomnessTransactionSubmissionBehaviour(RandomnessBehaviour): + """Retrieve randomness.""" + + matching_round = RandomnessTransactionSubmissionRound + payload_class = RandomnessPayload + + +class SelectKeeperTransactionSubmissionBehaviourA( # pylint: disable=too-many-ancestors + SelectKeeperBehaviour, TransactionSettlementBaseBehaviour +): + """Select the keeper agent.""" + + matching_round = SelectKeeperTransactionSubmissionARound + payload_class = SelectKeeperPayload + + def async_act(self) -> Generator: + """Do the action.""" + + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + keepers = deque((self._select_keeper(),)) + payload = self.payload_class( + self.context.agent_address, self.serialized_keepers(keepers, 1) + ) + + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + yield from self.send_a2a_transaction(payload) + yield from self.wait_until_round_end() + + self.set_done() + + +class SelectKeeperTransactionSubmissionBehaviourB( # pylint: disable=too-many-ancestors + SelectKeeperTransactionSubmissionBehaviourA +): + """Select the keeper b agent.""" + + matching_round = SelectKeeperTransactionSubmissionBRound + + def async_act(self) -> Generator: + """ + Do the action. + + Steps: + - If we have not selected enough keepers for the period, + select a keeper randomly and add it to the keepers' queue, with top priority. + - Otherwise, cycle through the keepers' subset, using the following logic: + A `PENDING` verification status means that we have not received any errors, + therefore, all we know is that the tx has not been mined yet due to low pricing. + Consequently, we are going to retry with the same keeper in order to replace the transaction. + However, if we receive a status other than `PENDING`, we need to cycle through the keepers' subset. + Moreover, if the current keeper has reached the allowed number of retries, then we cycle anyway. + - Send the transaction with the keepers and wait for it to be mined. + - Wait until ABCI application transitions to the next round. + - Go to the next behaviour (set done event). + """ + + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + keepers = self.synchronized_data.keepers + keeper_retries = 1 + + if self.synchronized_data.keepers_threshold_exceeded: + keepers.rotate(-1) + self.context.logger.info(f"Rotated keepers to: {keepers}.") + elif ( + self.synchronized_data.keeper_retries + != self.params.keeper_allowed_retries + and self.synchronized_data.final_verification_status + == VerificationStatus.PENDING + ): + keeper_retries += self.synchronized_data.keeper_retries + self.context.logger.info( + f"Kept keepers and incremented retries: {keepers}." + ) + else: + keepers.appendleft(self._select_keeper()) + + payload = self.payload_class( + self.context.agent_address, + self.serialized_keepers(keepers, keeper_retries), + ) + + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + yield from self.send_a2a_transaction(payload) + yield from self.wait_until_round_end() + + self.set_done() + + +class SelectKeeperTransactionSubmissionBehaviourBAfterTimeout( # pylint: disable=too-many-ancestors + SelectKeeperTransactionSubmissionBehaviourB +): + """Select the keeper b agent after a timeout.""" + + matching_round = SelectKeeperTransactionSubmissionBAfterTimeoutRound + + +class ValidateTransactionBehaviour(TransactionSettlementBaseBehaviour): + """Validate a transaction.""" + + matching_round = ValidateTransactionRound + + def async_act(self) -> Generator: + """ + Do the action. + + Steps: + - Validate that the transaction hash provided by the keeper points to a + valid transaction. + - Send the transaction with the validation result and wait for it to be + mined. + - Wait until ABCI application transitions to the next round. + - Go to the next behaviour (set done event). + """ + + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + is_correct = yield from self.has_transaction_been_sent() + if is_correct: + self.context.logger.info( + f"Finalized with transaction hash: {self.synchronized_data.to_be_validated_tx_hash}" + ) + payload = ValidatePayload(self.context.agent_address, is_correct) + + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + yield from self.send_a2a_transaction(payload) + yield from self.wait_until_round_end() + + self.set_done() + + def has_transaction_been_sent(self) -> Generator[None, None, Optional[bool]]: + """Transaction verification.""" + + to_be_validated_tx_hash = self.synchronized_data.to_be_validated_tx_hash + + response = yield from self.get_transaction_receipt( + to_be_validated_tx_hash, + self.params.retry_timeout, + self.params.retry_attempts, + chain_id=self.synchronized_data.get_chain_id(self.params.default_chain_id), + ) + if response is None: # pragma: nocover + self.context.logger.error( + f"tx {to_be_validated_tx_hash} receipt check timed out!" + ) + return None + + contract_api_msg = yield from self._verify_tx(to_be_validated_tx_hash) + if ( + contract_api_msg.performative != ContractApiMessage.Performative.STATE + ): # pragma: nocover + self.context.logger.error( + f"verify_tx unsuccessful! Received: {contract_api_msg}" + ) + return False + + verified = cast(bool, contract_api_msg.state.body["verified"]) + verified_log = ( + f"Verified result: {verified}" + if verified + else f"Verified result: {verified}, all: {contract_api_msg.state.body}" + ) + self.context.logger.info(verified_log) + return verified + + +class CheckTransactionHistoryBehaviour(TransactionSettlementBaseBehaviour): + """Check the transaction history.""" + + matching_round = CheckTransactionHistoryRound + check_expected_to_be_verified = "The next tx check" + + @property + def history(self) -> List[str]: + """Get the history of hashes.""" + return self.synchronized_data.tx_hashes_history + + def async_act(self) -> Generator: + """Do the action.""" + + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + verification_status, tx_hash = yield from self._check_tx_history() + if verification_status == VerificationStatus.VERIFIED: + msg = f"A previous transaction {tx_hash} has already been verified " + msg += ( + f"for {self.synchronized_data.to_be_validated_tx_hash}." + if self.synchronized_data.tx_hashes_history + else "and was synced after the finalization round timed out." + ) + self.context.logger.info(msg) + elif verification_status == VerificationStatus.NOT_VERIFIED: + self.context.logger.info( + f"No previous transaction has been verified for " + f"{self.synchronized_data.to_be_validated_tx_hash}." + ) + + verified_res = tx_hist_payload_to_hex(verification_status, tx_hash) + payload = CheckTransactionHistoryPayload( + self.context.agent_address, verified_res + ) + + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + yield from self.send_a2a_transaction(payload) + yield from self.wait_until_round_end() + + self.set_done() + + def _check_tx_history( # pylint: disable=too-many-return-statements + self, + ) -> Generator[None, None, Tuple[VerificationStatus, Optional[str]]]: + """Check the transaction history.""" + if not self.history: + self.context.logger.error( + "An unexpected error occurred! The synchronized data do not contain any transaction hashes, " + f"but entered the `{self.behaviour_id}` behaviour." + ) + return VerificationStatus.ERROR, None + + contract_api_msg = yield from self._get_safe_nonce() + if ( + contract_api_msg.performative != ContractApiMessage.Performative.STATE + ): # pragma: nocover + self.context.logger.error( + f"get_safe_nonce unsuccessful! Received: {contract_api_msg}" + ) + return VerificationStatus.ERROR, None + + safe_nonce = cast(int, contract_api_msg.state.body["safe_nonce"]) + if safe_nonce == self.params.mutable_params.nonce: + # if we have reached this state it means that the transaction didn't go through in the expected time + # as such we assume it is not verified + self.context.logger.info( + f"Safe nonce is the same as the nonce used in the transaction: {safe_nonce}. " + f"No transaction has gone through yet." + ) + return VerificationStatus.NOT_VERIFIED, None + + self.context.logger.info( + f"A transaction with nonce {safe_nonce} has already been sent. " + ) + self.context.logger.info( + f"Starting check for the transaction history: {self.history}. " + ) + was_nonce_reused = False + for tx_hash in self.history[::-1]: + self.context.logger.info(f"Checking hash {tx_hash}...") + contract_api_msg = yield from self._verify_tx(tx_hash) + + if ( + contract_api_msg.performative != ContractApiMessage.Performative.STATE + ): # pragma: nocover + self.context.logger.error( + f"verify_tx unsuccessful for {tx_hash}! Received: {contract_api_msg}" + ) + return VerificationStatus.ERROR, tx_hash + + verified = cast(bool, contract_api_msg.state.body["verified"]) + verified_log = f"Verified result for {tx_hash}: {verified}" + + if verified: + self.context.logger.info(verified_log) + return VerificationStatus.VERIFIED, tx_hash + + self.context.logger.info( + verified_log + f", all: {contract_api_msg.state.body}" + ) + + status = cast(int, contract_api_msg.state.body["status"]) + if status == -1: + self.context.logger.info(f"Tx hash {tx_hash} has no receipt!") + # this loop might take a long time + # we do not want to starve the rest of the behaviour + # we yield which freezes this loop here until the + # AbstractRoundBehaviour it belongs to, sends a tick to it + yield + continue + + tx_data = cast(TxData, contract_api_msg.state.body["transaction"]) + revert_reason = yield from self._get_revert_reason(tx_data) + + if revert_reason is not None: + if self._safe_nonce_reused(revert_reason): + self.context.logger.info( + f"The safe's nonce has been reused for {tx_hash}. " + f"{self.check_expected_to_be_verified} is expected to be verified!" + ) + was_nonce_reused = True + # this loop might take a long time + # we do not want to starve the rest of the behaviour + # we yield which freezes this loop here until the + # AbstractRoundBehaviour it belongs to, sends a tick to it + yield + continue + + self.context.logger.warning( + f"Payload is invalid for {tx_hash}! Cannot continue. Received: {revert_reason}" + ) + + return VerificationStatus.INVALID_PAYLOAD, tx_hash + + if was_nonce_reused: + self.context.logger.info( + f"Safe nonce {safe_nonce} was used, but no valid transaction was found. " + f"We cannot resend the transaction with the same nonce." + ) + return VerificationStatus.BAD_SAFE_NONCE, None + + return VerificationStatus.NOT_VERIFIED, None + + def _get_revert_reason(self, tx: TxData) -> Generator[None, None, Optional[str]]: + """Get the revert reason of the given transaction.""" + chain_id = self.synchronized_data.get_chain_id(self.params.default_chain_id) + contract_api_msg = yield from self.get_contract_api_response( + performative=ContractApiMessage.Performative.GET_STATE, # type: ignore + contract_address=self.synchronized_data.safe_contract_address, + contract_id=str(GnosisSafeContract.contract_id), + contract_callable="revert_reason", + tx=tx, + chain_id=chain_id, + ) + + if ( + contract_api_msg.performative != ContractApiMessage.Performative.STATE + ): # pragma: nocover + self.context.logger.error( + f"An unexpected error occurred while checking {tx['hash'].hex()}: {contract_api_msg}" + ) + return None + + return cast(str, contract_api_msg.state.body["revert_reason"]) + + +class CheckLateTxHashesBehaviour( # pylint: disable=too-many-ancestors + CheckTransactionHistoryBehaviour +): + """Check the late-arriving transaction hashes.""" + + matching_round = CheckLateTxHashesRound + check_expected_to_be_verified = "One of the next tx checks" + + @property + def history(self) -> List[str]: + """Get the history of hashes.""" + return [ + hash_ + for hashes in self.synchronized_data.late_arriving_tx_hashes.values() + for hash_ in hashes + ] + + +class SynchronizeLateMessagesBehaviour(TransactionSettlementBaseBehaviour): + """Synchronize late-arriving messages behaviour.""" + + matching_round = SynchronizeLateMessagesRound + + def __init__(self, **kwargs: Any): + """Initialize a `SynchronizeLateMessagesBehaviour`""" + super().__init__(**kwargs) + # if we timed out during finalization, but we managed to receive a tx hash, + # then we sync it here by initializing the `_tx_hashes` with the unsynced hash. + self._tx_hashes: str = self.params.mutable_params.tx_hash + self._messages_iterator: Iterator[ContractApiMessage] = iter( + self.params.mutable_params.late_messages + ) + self.use_flashbots = False + + def setup(self) -> None: + """Setup the `SynchronizeLateMessagesBehaviour`.""" + tx_params = skill_input_hex_to_payload( + self.synchronized_data.most_voted_tx_hash + ) + self.use_flashbots = tx_params["use_flashbots"] + + def async_act(self) -> Generator: + """Do the action.""" + + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + current_message = next(self._messages_iterator, None) + if current_message is not None: + chain_id = self.synchronized_data.get_chain_id( + self.params.default_chain_id + ) + tx_data = yield from self._get_tx_data( + current_message, + self.use_flashbots, + chain_id=chain_id, + ) + self.context.logger.info( + f"Found a late arriving message {current_message}. Result data: {tx_data}" + ) + # here, we concatenate the tx_hashes of all the late-arriving messages. Later, we will parse them. + self._tx_hashes += cast(str, tx_data["tx_digest"]) + return + + payload = SynchronizeLateMessagesPayload( + self.context.agent_address, self._tx_hashes + ) + + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + yield from self.send_a2a_transaction(payload) + # reset the local parameters if we were able to send them. + self.params.mutable_params.tx_hash = "" + self.params.mutable_params.late_messages = [] + yield from self.wait_until_round_end() + + self.set_done() + + +class SignatureBehaviour(TransactionSettlementBaseBehaviour): + """Signature behaviour.""" + + matching_round = CollectSignatureRound + + def async_act(self) -> Generator: + """ + Do the action. + + Steps: + - Request the signature of the transaction hash. + - Send the signature as a transaction and wait for it to be mined. + - Wait until ABCI application transitions to the next round. + - Go to the next behaviour (set done event). + """ + + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + self.context.logger.info( + f"Agreement reached on tx data: {self.synchronized_data.most_voted_tx_hash}" + ) + signature_hex = yield from self._get_safe_tx_signature() + payload = SignaturePayload(self.context.agent_address, signature_hex) + + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + yield from self.send_a2a_transaction(payload) + yield from self.wait_until_round_end() + + self.set_done() + + def _get_safe_tx_signature(self) -> Generator[None, None, str]: + """Get signature of safe transaction hash.""" + tx_params = skill_input_hex_to_payload( + self.synchronized_data.most_voted_tx_hash + ) + # is_deprecated_mode=True because we want to call Account.signHash, + # which is the same used by gnosis-py + safe_tx_hash_bytes = binascii.unhexlify(tx_params["safe_tx_hash"]) + signature_hex = yield from self.get_signature( + safe_tx_hash_bytes, is_deprecated_mode=True + ) + # remove the leading '0x' + signature_hex = signature_hex[2:] + self.context.logger.info(f"Signature: {signature_hex}") + return signature_hex + + +class FinalizeBehaviour(TransactionSettlementBaseBehaviour): + """Finalize behaviour.""" + + matching_round = FinalizationRound + + def _i_am_not_sending(self) -> bool: + """Indicates if the current agent is the sender or not.""" + return ( + self.context.agent_address + != self.synchronized_data.most_voted_keeper_address + ) + + def async_act(self) -> Generator[None, None, None]: + """ + Do the action. + + Steps: + - If the agent is the keeper, then prepare the transaction and send it. + - Otherwise, wait until the next round. + - If a timeout is hit, set exit A event, otherwise set done event. + """ + if self._i_am_not_sending(): + yield from self._not_sender_act() + else: + yield from self._sender_act() + + def _not_sender_act(self) -> Generator: + """Do the non-sender action.""" + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + self.context.logger.info( + f"Waiting for the keeper to do its keeping: {self.synchronized_data.most_voted_keeper_address}" + ) + yield from self.wait_until_round_end() + self.set_done() + + def _sender_act(self) -> Generator[None, None, None]: + """Do the sender action.""" + + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + self.context.logger.info( + "I am the designated sender, attempting to send the safe transaction..." + ) + tx_data = yield from self._send_safe_transaction() + if ( + tx_data["tx_digest"] == "" + and tx_data["status"] == VerificationStatus.PENDING + ) or tx_data["status"] == VerificationStatus.ERROR: + self.context.logger.error( + "Did not succeed with finalising the transaction!" + ) + elif tx_data["status"] == VerificationStatus.VERIFIED: + self.context.logger.error( + "Trying to finalize a transaction which has been verified already!" + ) + else: # pragma: no cover + self.context.logger.info( + f"Finalization tx digest: {cast(str, tx_data['tx_digest'])}" + ) + self.context.logger.debug( + f"Signatures: {pprint.pformat(self.synchronized_data.participant_to_signature)}" + ) + + tx_hashes_history = self.synchronized_data.tx_hashes_history + + if tx_data["tx_digest"] != "": + tx_hashes_history.append(cast(str, tx_data["tx_digest"])) + + tx_data_serialized = { + "status_value": cast(VerificationStatus, tx_data["status"]).value, + "serialized_keepers": self.serialized_keepers( + cast(Deque[str], tx_data["keepers"]), + cast(int, tx_data["keeper_retries"]), + ), + "blacklisted_keepers": "".join( + cast(Set[str], tx_data["blacklisted_keepers"]) + ), + "tx_hashes_history": "".join(tx_hashes_history), + "received_hash": bool(tx_data["tx_digest"]), + } + + payload = FinalizationTxPayload( + self.context.agent_address, + cast(Dict[str, Union[str, int, bool]], tx_data_serialized), + ) + + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + yield from self.send_a2a_transaction(payload) + # reset the local tx hash parameter if we were able to send it + self.params.mutable_params.tx_hash = "" + yield from self.wait_until_round_end() + + self.set_done() + + def _send_safe_transaction( + self, + ) -> Generator[None, None, TxDataType]: + """Send a Safe transaction using the participants' signatures.""" + tx_params = skill_input_hex_to_payload( + self.synchronized_data.most_voted_tx_hash + ) + chain_id = self.synchronized_data.get_chain_id(self.params.default_chain_id) + contract_api_msg = yield from self.get_contract_api_response( + performative=ContractApiMessage.Performative.GET_RAW_TRANSACTION, # type: ignore + contract_address=self.synchronized_data.safe_contract_address, + contract_id=str(GnosisSafeContract.contract_id), + contract_callable="get_raw_safe_transaction", + sender_address=self.context.agent_address, + owners=tuple(self.synchronized_data.participants), + to_address=tx_params["to_address"], + value=tx_params["ether_value"], + data=tx_params["data"], + safe_tx_gas=tx_params["safe_tx_gas"], + signatures_by_owner={ + key: payload.signature + for key, payload in self.synchronized_data.participant_to_signature.items() + }, + nonce=self.params.mutable_params.nonce, + old_price=self.params.mutable_params.gas_price, + operation=tx_params["operation"], + fallback_gas=self.params.mutable_params.fallback_gas, + gas_price=self.params.gas_params.gas_price, + max_fee_per_gas=self.params.gas_params.max_fee_per_gas, + max_priority_fee_per_gas=self.params.gas_params.max_priority_fee_per_gas, + chain_id=chain_id, + ) + + tx_data = yield from self._get_tx_data( + contract_api_msg, + tx_params["use_flashbots"], + tx_params["gas_limit"], + tx_params["raise_on_failed_simulation"], + chain_id, + ) + return tx_data + + def handle_late_messages(self, behaviour_id: str, message: Message) -> None: + """Store a potentially late-arriving message locally. + + :param behaviour_id: the id of the behaviour in which the message belongs to. + :param message: the late arriving message to handle. + """ + if ( + isinstance(message, ContractApiMessage) + and behaviour_id == self.behaviour_id + ): + self.context.logger.info(f"Late message arrived: {message}") + self.params.mutable_params.late_messages.append(message) + else: + super().handle_late_messages(behaviour_id, message) + + +class ResetBehaviour(TransactionSettlementBaseBehaviour): + """Reset behaviour.""" + + matching_round = ResetRound + + def async_act(self) -> Generator: + """Do the action.""" + self.context.logger.info( + f"Period {self.synchronized_data.period_count} was not finished. Resetting!" + ) + payload = ResetPayload( + self.context.agent_address, self.synchronized_data.period_count + ) + yield from self.send_a2a_transaction(payload) + yield from self.wait_until_round_end() + self.set_done() + + +class TransactionSettlementRoundBehaviour(AbstractRoundBehaviour): + """This behaviour manages the consensus stages for the basic transaction settlement.""" + + initial_behaviour_cls = RandomnessTransactionSubmissionBehaviour + abci_app_cls = TransactionSubmissionAbciApp + behaviours: Set[Type[BaseBehaviour]] = { + RandomnessTransactionSubmissionBehaviour, # type: ignore + SelectKeeperTransactionSubmissionBehaviourA, # type: ignore + SelectKeeperTransactionSubmissionBehaviourB, # type: ignore + SelectKeeperTransactionSubmissionBehaviourBAfterTimeout, # type: ignore + ValidateTransactionBehaviour, # type: ignore + CheckTransactionHistoryBehaviour, # type: ignore + SignatureBehaviour, # type: ignore + FinalizeBehaviour, # type: ignore + SynchronizeLateMessagesBehaviour, # type: ignore + CheckLateTxHashesBehaviour, # type: ignore + ResetBehaviour, # type: ignore + } diff --git a/packages/valory/skills/transaction_settlement_abci/dialogues.py b/packages/valory/skills/transaction_settlement_abci/dialogues.py new file mode 100644 index 0000000..e244bde --- /dev/null +++ b/packages/valory/skills/transaction_settlement_abci/dialogues.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the classes required for dialogue management.""" + +from packages.valory.skills.abstract_round_abci.dialogues import ( + AbciDialogue as BaseAbciDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + AbciDialogues as BaseAbciDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + ContractApiDialogue as BaseContractApiDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + ContractApiDialogues as BaseContractApiDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + HttpDialogue as BaseHttpDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + HttpDialogues as BaseHttpDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + IpfsDialogue as BaseIpfsDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + IpfsDialogues as BaseIpfsDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + LedgerApiDialogue as BaseLedgerApiDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + LedgerApiDialogues as BaseLedgerApiDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + SigningDialogue as BaseSigningDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + SigningDialogues as BaseSigningDialogues, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + TendermintDialogue as BaseTendermintDialogue, +) +from packages.valory.skills.abstract_round_abci.dialogues import ( + TendermintDialogues as BaseTendermintDialogues, +) + + +AbciDialogue = BaseAbciDialogue +AbciDialogues = BaseAbciDialogues + + +HttpDialogue = BaseHttpDialogue +HttpDialogues = BaseHttpDialogues + + +SigningDialogue = BaseSigningDialogue +SigningDialogues = BaseSigningDialogues + + +LedgerApiDialogue = BaseLedgerApiDialogue +LedgerApiDialogues = BaseLedgerApiDialogues + + +ContractApiDialogue = BaseContractApiDialogue +ContractApiDialogues = BaseContractApiDialogues + + +TendermintDialogue = BaseTendermintDialogue +TendermintDialogues = BaseTendermintDialogues + + +IpfsDialogue = BaseIpfsDialogue +IpfsDialogues = BaseIpfsDialogues diff --git a/packages/valory/skills/transaction_settlement_abci/fsm_specification.yaml b/packages/valory/skills/transaction_settlement_abci/fsm_specification.yaml new file mode 100644 index 0000000..70b6e1f --- /dev/null +++ b/packages/valory/skills/transaction_settlement_abci/fsm_specification.yaml @@ -0,0 +1,88 @@ +alphabet_in: +- CHECK_HISTORY +- CHECK_LATE_ARRIVING_MESSAGE +- CHECK_TIMEOUT +- DONE +- FINALIZATION_FAILED +- FINALIZE_TIMEOUT +- INCORRECT_SERIALIZATION +- INSUFFICIENT_FUNDS +- NEGATIVE +- NONE +- NO_MAJORITY +- RESET_TIMEOUT +- ROUND_TIMEOUT +- SUSPICIOUS_ACTIVITY +- VALIDATE_TIMEOUT +default_start_state: RandomnessTransactionSubmissionRound +final_states: +- FailedRound +- FinishedTransactionSubmissionRound +label: TransactionSubmissionAbciApp +start_states: +- RandomnessTransactionSubmissionRound +states: +- CheckLateTxHashesRound +- CheckTransactionHistoryRound +- CollectSignatureRound +- FailedRound +- FinalizationRound +- FinishedTransactionSubmissionRound +- RandomnessTransactionSubmissionRound +- ResetRound +- SelectKeeperTransactionSubmissionARound +- SelectKeeperTransactionSubmissionBAfterTimeoutRound +- SelectKeeperTransactionSubmissionBRound +- SynchronizeLateMessagesRound +- ValidateTransactionRound +transition_func: + (CheckLateTxHashesRound, CHECK_LATE_ARRIVING_MESSAGE): SynchronizeLateMessagesRound + (CheckLateTxHashesRound, CHECK_TIMEOUT): CheckLateTxHashesRound + (CheckLateTxHashesRound, DONE): FinishedTransactionSubmissionRound + (CheckLateTxHashesRound, NEGATIVE): FailedRound + (CheckLateTxHashesRound, NONE): FailedRound + (CheckLateTxHashesRound, NO_MAJORITY): FailedRound + (CheckTransactionHistoryRound, CHECK_LATE_ARRIVING_MESSAGE): SynchronizeLateMessagesRound + (CheckTransactionHistoryRound, CHECK_TIMEOUT): CheckTransactionHistoryRound + (CheckTransactionHistoryRound, DONE): FinishedTransactionSubmissionRound + (CheckTransactionHistoryRound, NEGATIVE): SelectKeeperTransactionSubmissionBRound + (CheckTransactionHistoryRound, NONE): FailedRound + (CheckTransactionHistoryRound, NO_MAJORITY): CheckTransactionHistoryRound + (CollectSignatureRound, DONE): FinalizationRound + (CollectSignatureRound, NO_MAJORITY): ResetRound + (CollectSignatureRound, ROUND_TIMEOUT): CollectSignatureRound + (FinalizationRound, CHECK_HISTORY): CheckTransactionHistoryRound + (FinalizationRound, CHECK_LATE_ARRIVING_MESSAGE): SynchronizeLateMessagesRound + (FinalizationRound, DONE): ValidateTransactionRound + (FinalizationRound, FINALIZATION_FAILED): SelectKeeperTransactionSubmissionBRound + (FinalizationRound, FINALIZE_TIMEOUT): SelectKeeperTransactionSubmissionBAfterTimeoutRound + (FinalizationRound, INSUFFICIENT_FUNDS): SelectKeeperTransactionSubmissionBRound + (RandomnessTransactionSubmissionRound, DONE): SelectKeeperTransactionSubmissionARound + (RandomnessTransactionSubmissionRound, NO_MAJORITY): RandomnessTransactionSubmissionRound + (RandomnessTransactionSubmissionRound, ROUND_TIMEOUT): RandomnessTransactionSubmissionRound + (ResetRound, DONE): RandomnessTransactionSubmissionRound + (ResetRound, NO_MAJORITY): FailedRound + (ResetRound, RESET_TIMEOUT): FailedRound + (SelectKeeperTransactionSubmissionARound, DONE): CollectSignatureRound + (SelectKeeperTransactionSubmissionARound, INCORRECT_SERIALIZATION): FailedRound + (SelectKeeperTransactionSubmissionARound, NO_MAJORITY): ResetRound + (SelectKeeperTransactionSubmissionARound, ROUND_TIMEOUT): SelectKeeperTransactionSubmissionARound + (SelectKeeperTransactionSubmissionBAfterTimeoutRound, CHECK_HISTORY): CheckTransactionHistoryRound + (SelectKeeperTransactionSubmissionBAfterTimeoutRound, CHECK_LATE_ARRIVING_MESSAGE): SynchronizeLateMessagesRound + (SelectKeeperTransactionSubmissionBAfterTimeoutRound, DONE): FinalizationRound + (SelectKeeperTransactionSubmissionBAfterTimeoutRound, INCORRECT_SERIALIZATION): FailedRound + (SelectKeeperTransactionSubmissionBAfterTimeoutRound, NO_MAJORITY): ResetRound + (SelectKeeperTransactionSubmissionBAfterTimeoutRound, ROUND_TIMEOUT): SelectKeeperTransactionSubmissionBAfterTimeoutRound + (SelectKeeperTransactionSubmissionBRound, DONE): FinalizationRound + (SelectKeeperTransactionSubmissionBRound, INCORRECT_SERIALIZATION): FailedRound + (SelectKeeperTransactionSubmissionBRound, NO_MAJORITY): ResetRound + (SelectKeeperTransactionSubmissionBRound, ROUND_TIMEOUT): SelectKeeperTransactionSubmissionBRound + (SynchronizeLateMessagesRound, DONE): CheckLateTxHashesRound + (SynchronizeLateMessagesRound, NONE): SelectKeeperTransactionSubmissionBRound + (SynchronizeLateMessagesRound, ROUND_TIMEOUT): SynchronizeLateMessagesRound + (SynchronizeLateMessagesRound, SUSPICIOUS_ACTIVITY): FailedRound + (ValidateTransactionRound, DONE): FinishedTransactionSubmissionRound + (ValidateTransactionRound, NEGATIVE): CheckTransactionHistoryRound + (ValidateTransactionRound, NONE): SelectKeeperTransactionSubmissionBRound + (ValidateTransactionRound, NO_MAJORITY): ValidateTransactionRound + (ValidateTransactionRound, VALIDATE_TIMEOUT): CheckTransactionHistoryRound diff --git a/packages/valory/skills/transaction_settlement_abci/handlers.py b/packages/valory/skills/transaction_settlement_abci/handlers.py new file mode 100644 index 0000000..9a4990b --- /dev/null +++ b/packages/valory/skills/transaction_settlement_abci/handlers.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the handler for the 'transaction_settlement_abci' skill.""" + +from packages.valory.skills.abstract_round_abci.handlers import ( + ABCIRoundHandler as BaseABCIRoundHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + ContractApiHandler as BaseContractApiHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + HttpHandler as BaseHttpHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + IpfsHandler as BaseIpfsHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + LedgerApiHandler as BaseLedgerApiHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + SigningHandler as BaseSigningHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import ( + TendermintHandler as BaseTendermintHandler, +) + + +ABCIHandler = BaseABCIRoundHandler +HttpHandler = BaseHttpHandler +SigningHandler = BaseSigningHandler +LedgerApiHandler = BaseLedgerApiHandler +ContractApiHandler = BaseContractApiHandler +TendermintHandler = BaseTendermintHandler +IpfsHandler = BaseIpfsHandler diff --git a/packages/valory/skills/transaction_settlement_abci/models.py b/packages/valory/skills/transaction_settlement_abci/models.py new file mode 100644 index 0000000..e65bb69 --- /dev/null +++ b/packages/valory/skills/transaction_settlement_abci/models.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Custom objects for the transaction settlement ABCI application.""" +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional + +from web3.types import Nonce, Wei + +from packages.valory.protocols.contract_api import ContractApiMessage +from packages.valory.skills.abstract_round_abci.models import ApiSpecs, BaseParams +from packages.valory.skills.abstract_round_abci.models import ( + BenchmarkTool as BaseBenchmarkTool, +) +from packages.valory.skills.abstract_round_abci.models import Requests as BaseRequests +from packages.valory.skills.abstract_round_abci.models import ( + SharedState as BaseSharedState, +) +from packages.valory.skills.abstract_round_abci.models import TypeCheckMixin +from packages.valory.skills.transaction_settlement_abci.rounds import ( + TransactionSubmissionAbciApp, +) + + +_MINIMUM_VALIDATE_TIMEOUT = 300 # 5 minutes +BenchmarkTool = BaseBenchmarkTool + + +class SharedState(BaseSharedState): + """Keep the current shared state of the skill.""" + + abci_app_cls = TransactionSubmissionAbciApp + + +@dataclass +class MutableParams(TypeCheckMixin): + """Collection for the mutable parameters.""" + + fallback_gas: int + tx_hash: str = "" + nonce: Optional[Nonce] = None + gas_price: Optional[Dict[str, Wei]] = None + late_messages: List[ContractApiMessage] = field(default_factory=list) + + +@dataclass +class GasParams(BaseParams): + """Gas parameters.""" + + gas_price: Optional[int] = None + max_fee_per_gas: Optional[int] = None + max_priority_fee_per_gas: Optional[int] = None + + +class TransactionParams(BaseParams): # pylint: disable=too-many-instance-attributes + """Transaction settlement agent-specific parameters.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """ + Initialize the parameters object. + + We keep track of the nonce and tip across rounds and periods. + We reuse it each time a new raw transaction is generated. If + at the time of the new raw transaction being generated the nonce + on the ledger does not match the nonce on the skill, then we ignore + the skill nonce and tip (effectively we price fresh). Otherwise, we + are in a re-submission scenario where we need to take account of the + old tip. + + :param args: positional arguments + :param kwargs: keyword arguments + """ + self.mutable_params = MutableParams( + fallback_gas=self._ensure("init_fallback_gas", kwargs, int) + ) + self.keeper_allowed_retries: int = self._ensure( + "keeper_allowed_retries", kwargs, int + ) + self.validate_timeout: int = self._ensure_gte( + "validate_timeout", + kwargs, + int, + min_value=_MINIMUM_VALIDATE_TIMEOUT, + ) + self.finalize_timeout: float = self._ensure("finalize_timeout", kwargs, float) + self.history_check_timeout: int = self._ensure( + "history_check_timeout", kwargs, int + ) + self.gas_params = self._get_gas_params(kwargs) + super().__init__(*args, **kwargs) + + @staticmethod + def _get_gas_params(kwargs: Dict[str, Any]) -> GasParams: + """Get the gas parameters.""" + gas_params = kwargs.pop("gas_params", {}) + gas_price = gas_params.get("gas_price", None) + max_fee_per_gas = gas_params.get("max_fee_per_gas", None) + max_priority_fee_per_gas = gas_params.get("max_priority_fee_per_gas", None) + return GasParams( + gas_price=gas_price, + max_fee_per_gas=max_fee_per_gas, + max_priority_fee_per_gas=max_priority_fee_per_gas, + ) + + +RandomnessApi = ApiSpecs +Requests = BaseRequests diff --git a/packages/valory/skills/transaction_settlement_abci/payload_tools.py b/packages/valory/skills/transaction_settlement_abci/payload_tools.py new file mode 100644 index 0000000..44a8688 --- /dev/null +++ b/packages/valory/skills/transaction_settlement_abci/payload_tools.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2022-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Tools for payload serialization and deserialization.""" + +from enum import Enum +from typing import Any, Optional, Tuple + +from packages.valory.contracts.gnosis_safe.contract import SafeOperation + + +NULL_ADDRESS: str = "0x" + "0" * 40 +MAX_UINT256 = 2**256 - 1 + + +class VerificationStatus(Enum): + """Tx verification status enumeration.""" + + VERIFIED = 1 + NOT_VERIFIED = 2 + INVALID_PAYLOAD = 3 + PENDING = 4 + ERROR = 5 + INSUFFICIENT_FUNDS = 6 + BAD_SAFE_NONCE = 7 + + +class PayloadDeserializationError(Exception): + """Exception for payload deserialization errors.""" + + def __init__(self, *args: Any) -> None: + """Initialize the exception. + + :param args: extra arguments to pass to the constructor of `Exception`. + """ + msg: str = "Cannot decode provided payload!" + if not args: + args = (msg,) + + super().__init__(*args) + + +def tx_hist_payload_to_hex( + verification: VerificationStatus, tx_hash: Optional[str] = None +) -> str: + """Serialise history payload to a hex string.""" + if tx_hash is None: + tx_hash = "" + else: + tx_hash = tx_hash[2:] if tx_hash.startswith("0x") else tx_hash + if len(tx_hash) != 64: + raise ValueError("Cannot encode tx_hash of non-32 bytes") + verification_ = verification.value.to_bytes(32, "big").hex() + concatenated = verification_ + tx_hash + return concatenated + + +def tx_hist_hex_to_payload(payload: str) -> Tuple[VerificationStatus, Optional[str]]: + """Decode history payload.""" + if len(payload) != 64 and len(payload) != 64 * 2: + raise PayloadDeserializationError() + + verification_value = int.from_bytes(bytes.fromhex(payload[:64]), "big") + + try: + verification_status = VerificationStatus(verification_value) + except ValueError as e: + raise PayloadDeserializationError(str(e)) from e + + if len(payload) == 64: + return verification_status, None + + return verification_status, "0x" + payload[64:] + + +def hash_payload_to_hex( # pylint: disable=too-many-arguments, too-many-locals + safe_tx_hash: str, + ether_value: int, + safe_tx_gas: int, + to_address: str, + data: bytes, + operation: int = SafeOperation.CALL.value, + base_gas: int = 0, + safe_gas_price: int = 0, + gas_token: str = NULL_ADDRESS, + refund_receiver: str = NULL_ADDRESS, + use_flashbots: bool = False, + gas_limit: int = 0, + raise_on_failed_simulation: bool = False, +) -> str: + """Serialise to a hex string.""" + if len(safe_tx_hash) != 64: # should be exactly 32 bytes! + raise ValueError( + "cannot encode safe_tx_hash of non-32 bytes" + ) # pragma: nocover + + if len(to_address) != 42 or len(gas_token) != 42 or len(refund_receiver) != 42: + raise ValueError("cannot encode address of non 42 length") # pragma: nocover + + if ( + ether_value > MAX_UINT256 + or safe_tx_gas > MAX_UINT256 + or base_gas > MAX_UINT256 + or safe_gas_price > MAX_UINT256 + or gas_limit > MAX_UINT256 + ): + raise ValueError( + "Value is bigger than the max 256 bit value" + ) # pragma: nocover + + if operation not in [v.value for v in SafeOperation]: + raise ValueError("SafeOperation value is not valid") # pragma: nocover + + if not isinstance(use_flashbots, bool): + raise ValueError( + f"`use_flashbots` value ({use_flashbots}) is not valid. A boolean value was expected instead" + ) + + ether_value_ = ether_value.to_bytes(32, "big").hex() + safe_tx_gas_ = safe_tx_gas.to_bytes(32, "big").hex() + operation_ = operation.to_bytes(1, "big").hex() + base_gas_ = base_gas.to_bytes(32, "big").hex() + safe_gas_price_ = safe_gas_price.to_bytes(32, "big").hex() + use_flashbots_ = use_flashbots.to_bytes(32, "big").hex() + gas_limit_ = gas_limit.to_bytes(32, "big").hex() + raise_on_failed_simulation_ = raise_on_failed_simulation.to_bytes(32, "big").hex() + + concatenated = ( + safe_tx_hash + + ether_value_ + + safe_tx_gas_ + + to_address + + operation_ + + base_gas_ + + safe_gas_price_ + + gas_token + + refund_receiver + + use_flashbots_ + + gas_limit_ + + raise_on_failed_simulation_ + + data.hex() + ) + return concatenated + + +def skill_input_hex_to_payload(payload: str) -> dict: + """Decode payload.""" + if len(payload) < 234: + raise PayloadDeserializationError() # pragma: nocover + tx_params = dict( + safe_tx_hash=payload[:64], + ether_value=int.from_bytes(bytes.fromhex(payload[64:128]), "big"), + safe_tx_gas=int.from_bytes(bytes.fromhex(payload[128:192]), "big"), + to_address=payload[192:234], + operation=int.from_bytes(bytes.fromhex(payload[234:236]), "big"), + base_gas=int.from_bytes(bytes.fromhex(payload[236:300]), "big"), + safe_gas_price=int.from_bytes(bytes.fromhex(payload[300:364]), "big"), + gas_token=payload[364:406], + refund_receiver=payload[406:448], + use_flashbots=bool.from_bytes(bytes.fromhex(payload[448:512]), "big"), + gas_limit=int.from_bytes(bytes.fromhex(payload[512:576]), "big"), + raise_on_failed_simulation=bool.from_bytes( + bytes.fromhex(payload[576:640]), "big" + ), + data=bytes.fromhex(payload[640:]), + ) + return tx_params diff --git a/packages/valory/skills/transaction_settlement_abci/payloads.py b/packages/valory/skills/transaction_settlement_abci/payloads.py new file mode 100644 index 0000000..16dad1e --- /dev/null +++ b/packages/valory/skills/transaction_settlement_abci/payloads.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the transaction payloads for common apps.""" + +from dataclasses import dataclass +from typing import Dict, Optional, Union + +from packages.valory.skills.abstract_round_abci.base import BaseTxPayload + + +@dataclass(frozen=True) +class RandomnessPayload(BaseTxPayload): + """Represent a transaction payload of type 'randomness'.""" + + round_id: int + randomness: str + + +@dataclass(frozen=True) +class SelectKeeperPayload(BaseTxPayload): + """Represent a transaction payload of type 'select_keeper'.""" + + keepers: str + + +@dataclass(frozen=True) +class ValidatePayload(BaseTxPayload): + """Represent a transaction payload of type 'validate'.""" + + vote: Optional[bool] = None + + +@dataclass(frozen=True) +class CheckTransactionHistoryPayload(BaseTxPayload): + """Represent a transaction payload of type 'check'.""" + + verified_res: str + + +@dataclass(frozen=True) +class SynchronizeLateMessagesPayload(BaseTxPayload): + """Represent a transaction payload of type 'synchronize'.""" + + tx_hashes: str + + +@dataclass(frozen=True) +class SignaturePayload(BaseTxPayload): + """Represent a transaction payload of type 'signature'.""" + + signature: str + + +@dataclass(frozen=True) +class FinalizationTxPayload(BaseTxPayload): + """Represent a transaction payload of type 'finalization'.""" + + tx_data: Optional[Dict[str, Union[str, int, bool]]] = None + + +@dataclass(frozen=True) +class ResetPayload(BaseTxPayload): + """Represent a transaction payload of type 'reset'.""" + + period_count: int diff --git a/packages/valory/skills/transaction_settlement_abci/rounds.py b/packages/valory/skills/transaction_settlement_abci/rounds.py new file mode 100644 index 0000000..d6231ee --- /dev/null +++ b/packages/valory/skills/transaction_settlement_abci/rounds.py @@ -0,0 +1,831 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the data classes for the `transaction settlement` ABCI application.""" + +import textwrap +from abc import ABC +from collections import deque +from enum import Enum +from typing import Deque, Dict, List, Mapping, Optional, Set, Tuple, cast + +from packages.valory.skills.abstract_round_abci.base import ( + ABCIAppInternalError, + AbciApp, + AbciAppTransitionFunction, + AppState, + BaseSynchronizedData, + BaseTxPayload, + CollectDifferentUntilThresholdRound, + CollectNonEmptyUntilThresholdRound, + CollectSameUntilThresholdRound, + CollectionRound, + DegenerateRound, + OnlyKeeperSendsRound, + TransactionNotValidError, + VALUE_NOT_PROVIDED, + VotingRound, + get_name, +) +from packages.valory.skills.abstract_round_abci.utils import filter_negative +from packages.valory.skills.transaction_settlement_abci.payload_tools import ( + VerificationStatus, + tx_hist_hex_to_payload, +) +from packages.valory.skills.transaction_settlement_abci.payloads import ( + CheckTransactionHistoryPayload, + FinalizationTxPayload, + RandomnessPayload, + ResetPayload, + SelectKeeperPayload, + SignaturePayload, + SynchronizeLateMessagesPayload, + ValidatePayload, +) + + +ADDRESS_LENGTH = 42 +TX_HASH_LENGTH = 66 +RETRIES_LENGTH = 64 + + +class Event(Enum): + """Event enumeration for the price estimation demo.""" + + DONE = "done" + ROUND_TIMEOUT = "round_timeout" + NO_MAJORITY = "no_majority" + NEGATIVE = "negative" + NONE = "none" + FINALIZE_TIMEOUT = "finalize_timeout" + VALIDATE_TIMEOUT = "validate_timeout" + CHECK_TIMEOUT = "check_timeout" + RESET_TIMEOUT = "reset_timeout" + CHECK_HISTORY = "check_history" + CHECK_LATE_ARRIVING_MESSAGE = "check_late_arriving_message" + FINALIZATION_FAILED = "finalization_failed" + SUSPICIOUS_ACTIVITY = "suspicious_activity" + INSUFFICIENT_FUNDS = "insufficient_funds" + INCORRECT_SERIALIZATION = "incorrect_serialization" + + +class SynchronizedData( + BaseSynchronizedData +): # pylint: disable=too-many-instance-attributes + """ + Class to represent the synchronized data. + + This data is replicated by the tendermint application. + """ + + @property + def participant_to_signature(self) -> Mapping[str, SignaturePayload]: + """Get the participant_to_signature.""" + serialized = self.db.get_strict("participant_to_signature") + deserialized = CollectionRound.deserialize_collection(serialized) + return cast(Mapping[str, SignaturePayload], deserialized) + + @property + def tx_hashes_history(self) -> List[str]: + """Get the current cycle's tx hashes history, which has not yet been verified.""" + raw = cast(str, self.db.get("tx_hashes_history", "")) + return textwrap.wrap(raw, TX_HASH_LENGTH) + + @property + def keepers(self) -> Deque[str]: + """Get the current cycle's keepers who have tried to submit a transaction.""" + if self.is_keeper_set: + keepers_unparsed = cast(str, self.db.get_strict("keepers")) + keepers_parsed = textwrap.wrap( + keepers_unparsed[RETRIES_LENGTH:], ADDRESS_LENGTH + ) + return deque(keepers_parsed) + return deque() + + @property + def keepers_threshold_exceeded(self) -> bool: + """Check if the number of selected keepers has exceeded the allowed limit.""" + malicious_threshold = self.nb_participants // 3 + return len(self.keepers) > malicious_threshold + + @property + def most_voted_randomness_round(self) -> int: # pragma: no cover + """Get the first in priority keeper to try to re-submit a transaction.""" + round_ = self.db.get_strict("most_voted_randomness_round") + return cast(int, round_) + + @property + def most_voted_keeper_address(self) -> str: + """Get the first in priority keeper to try to re-submit a transaction.""" + return self.keepers[0] + + @property # TODO: overrides base property, investigate + def is_keeper_set(self) -> bool: + """Check whether keeper is set.""" + return bool(self.db.get("keepers", False)) + + @property + def keeper_retries(self) -> int: + """Get the number of times the current keeper has retried.""" + if self.is_keeper_set: + keepers_unparsed = cast(str, self.db.get_strict("keepers")) + keeper_retries = int.from_bytes( + bytes.fromhex(keepers_unparsed[:RETRIES_LENGTH]), "big" + ) + return keeper_retries + return 0 + + @property + def to_be_validated_tx_hash(self) -> str: + """ + Get the tx hash which is ready for validation. + + This will always be the last hash in the `tx_hashes_history`, + due to the way we are inserting the hashes in the array. + We keep the hashes sorted by the time of their finalization. + If this property is accessed before the finalization succeeds, + then it is incorrectly used and raises an error. + + :return: the tx hash which is ready for validation. + """ + if not self.tx_hashes_history: + raise ValueError( + "FSM design error: tx hash should exist" + ) # pragma: no cover + return self.tx_hashes_history[-1] + + @property + def final_tx_hash(self) -> str: + """Get the verified tx hash.""" + return cast(str, self.db.get_strict("final_tx_hash")) + + @property + def final_verification_status(self) -> VerificationStatus: + """Get the final verification status.""" + status_value = self.db.get("final_verification_status", None) + if status_value is None: + return VerificationStatus.NOT_VERIFIED + return VerificationStatus(status_value) + + @property + def most_voted_tx_hash(self) -> str: + """Get the most_voted_tx_hash.""" + return cast(str, self.db.get_strict("most_voted_tx_hash")) + + @property + def missed_messages(self) -> Dict[str, int]: + """The number of missed messages per agent address.""" + default = dict.fromkeys(self.all_participants, 0) + missed_messages = self.db.get("missed_messages", default) + return cast(Dict[str, int], missed_messages) + + @property + def n_missed_messages(self) -> int: + """The number of missed messages in total.""" + return sum(self.missed_messages.values()) + + @property + def should_check_late_messages(self) -> bool: + """Check if we should check for late-arriving messages.""" + return self.n_missed_messages > 0 + + @property + def late_arriving_tx_hashes(self) -> Dict[str, List[str]]: + """Get the late_arriving_tx_hashes.""" + late_arrivals = cast( + Dict[str, str], self.db.get_strict("late_arriving_tx_hashes") + ) + parsed_hashes = { + sender: textwrap.wrap(hashes, TX_HASH_LENGTH) + for sender, hashes in late_arrivals.items() + } + return parsed_hashes + + @property + def suspects(self) -> Tuple[str]: + """Get the suspect agents.""" + return cast(Tuple[str], self.db.get("suspects", tuple())) + + @property + def most_voted_check_result(self) -> str: # pragma: no cover + """Get the most voted checked result.""" + return cast(str, self.db.get_strict("most_voted_check_result")) + + @property + def participant_to_check( + self, + ) -> Mapping[str, CheckTransactionHistoryPayload]: # pragma: no cover + """Get the mapping from participants to checks.""" + serialized = self.db.get_strict("participant_to_check") + deserialized = CollectionRound.deserialize_collection(serialized) + return cast(Mapping[str, CheckTransactionHistoryPayload], deserialized) + + @property + def participant_to_late_messages( + self, + ) -> Mapping[str, SynchronizeLateMessagesPayload]: # pragma: no cover + """Get the mapping from participants to checks.""" + serialized = self.db.get_strict("participant_to_late_message") + deserialized = CollectionRound.deserialize_collection(serialized) + return cast(Mapping[str, SynchronizeLateMessagesPayload], deserialized) + + def get_chain_id(self, default_chain_id: str) -> str: + """Get the chain id.""" + return cast(str, self.db.get("chain_id", default_chain_id)) + + +class FailedRound(DegenerateRound, ABC): + """A round that represents that the period failed""" + + +class CollectSignatureRound(CollectDifferentUntilThresholdRound): + """A round in which agents sign the transaction""" + + payload_class = SignaturePayload + synchronized_data_class = SynchronizedData + done_event = Event.DONE + no_majority_event = Event.NO_MAJORITY + collection_key = get_name(SynchronizedData.participant_to_signature) + + +class FinalizationRound(OnlyKeeperSendsRound): + """A round that represents transaction signing has finished""" + + keeper_payload: Optional[FinalizationTxPayload] = None + payload_class = FinalizationTxPayload + synchronized_data_class = SynchronizedData + + def end_block( # pylint: disable=too-many-return-statements + self, + ) -> Optional[ + Tuple[BaseSynchronizedData, Enum] + ]: # pylint: disable=too-many-return-statements + """Process the end of the block.""" + if self.keeper_payload is None: + return None + + if self.keeper_payload.tx_data is None: + return self.synchronized_data, Event.FINALIZATION_FAILED + + verification_status = VerificationStatus( + self.keeper_payload.tx_data["status_value"] + ) + synchronized_data = cast( + SynchronizedData, + self.synchronized_data.update( + synchronized_data_class=self.synchronized_data_class, + **{ + get_name( + SynchronizedData.tx_hashes_history + ): self.keeper_payload.tx_data["tx_hashes_history"], + get_name( + SynchronizedData.final_verification_status + ): verification_status.value, + get_name(SynchronizedData.keepers): self.keeper_payload.tx_data[ + "serialized_keepers" + ], + get_name( + SynchronizedData.blacklisted_keepers + ): self.keeper_payload.tx_data["blacklisted_keepers"], + }, + ), + ) + + # check if we succeeded in finalization. + # we may fail in any of the following cases: + # 1. Getting raw safe transaction. + # 2. Requesting transaction signature. + # 3. Requesting transaction digest. + if self.keeper_payload.tx_data["received_hash"]: + return synchronized_data, Event.DONE + # If keeper has been blacklisted, return an `INSUFFICIENT_FUNDS` event. + if verification_status == VerificationStatus.INSUFFICIENT_FUNDS: + return synchronized_data, Event.INSUFFICIENT_FUNDS + # This means that getting raw safe transaction succeeded, + # but either requesting tx signature or requesting tx digest failed. + if verification_status not in ( + VerificationStatus.ERROR, + VerificationStatus.VERIFIED, + ): + return synchronized_data, Event.FINALIZATION_FAILED + # if there is a tx hash history, then check it for validated txs. + if synchronized_data.tx_hashes_history: + return synchronized_data, Event.CHECK_HISTORY + # if there could be any late messages, check if any has arrived. + if synchronized_data.should_check_late_messages: + return synchronized_data, Event.CHECK_LATE_ARRIVING_MESSAGE + # otherwise fail. + return synchronized_data, Event.FINALIZATION_FAILED + + +class RandomnessTransactionSubmissionRound(CollectSameUntilThresholdRound): + """A round for generating randomness""" + + payload_class = RandomnessPayload + synchronized_data_class = SynchronizedData + done_event = Event.DONE + no_majority_event = Event.NO_MAJORITY + collection_key = get_name(SynchronizedData.participant_to_randomness) + selection_key = ( + get_name(SynchronizedData.most_voted_randomness_round), + get_name(SynchronizedData.most_voted_randomness), + ) + + +class SelectKeeperTransactionSubmissionARound(CollectSameUntilThresholdRound): + """A round in which a keeper is selected for transaction submission""" + + payload_class = SelectKeeperPayload + synchronized_data_class = SynchronizedData + done_event = Event.DONE + no_majority_event = Event.NO_MAJORITY + collection_key = get_name(SynchronizedData.participant_to_selection) + selection_key = get_name(SynchronizedData.keepers) + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: + """Process the end of the block.""" + + if self.threshold_reached and self.most_voted_payload is not None: + if ( + len(self.most_voted_payload) < RETRIES_LENGTH + ADDRESS_LENGTH + or (len(self.most_voted_payload) - RETRIES_LENGTH) % ADDRESS_LENGTH != 0 + ): + # if we cannot parse the keepers' payload, then the developer has serialized it incorrectly. + return self.synchronized_data, Event.INCORRECT_SERIALIZATION + + return super().end_block() + + +class SelectKeeperTransactionSubmissionBRound(SelectKeeperTransactionSubmissionARound): + """A round in which a new keeper is selected for transaction submission""" + + +class SelectKeeperTransactionSubmissionBAfterTimeoutRound( + SelectKeeperTransactionSubmissionBRound +): + """A round in which a new keeper is selected for tx submission after a round timeout of the previous keeper""" + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: + """Process the end of the block.""" + if self.threshold_reached: + synchronized_data = cast(SynchronizedData, self.synchronized_data) + keeper = synchronized_data.most_voted_keeper_address + missed_messages = synchronized_data.missed_messages + missed_messages[keeper] += 1 + + synchronized_data = cast( + SynchronizedData, + self.synchronized_data.update( + synchronized_data_class=self.synchronized_data_class, + **{get_name(SynchronizedData.missed_messages): missed_messages}, + ), + ) + if synchronized_data.keepers_threshold_exceeded: + # we only stop re-selection if there are any previous transaction hashes or any missed messages. + if len(synchronized_data.tx_hashes_history) > 0: + return synchronized_data, Event.CHECK_HISTORY + if synchronized_data.should_check_late_messages: + return synchronized_data, Event.CHECK_LATE_ARRIVING_MESSAGE + return super().end_block() + + +class ValidateTransactionRound(VotingRound): + """A round in which agents validate the transaction""" + + payload_class = ValidatePayload + synchronized_data_class = SynchronizedData + done_event = Event.DONE + negative_event = Event.NEGATIVE + none_event = Event.NONE + no_majority_event = Event.NO_MAJORITY + collection_key = get_name(SynchronizedData.participant_to_votes) + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: + """Process the end of the block.""" + # if reached participant threshold, set the result + + if self.positive_vote_threshold_reached: + # We obtain the latest tx hash from the `tx_hashes_history`. + # We keep the hashes sorted by their finalization time. + # If this property is accessed before the finalization succeeds, + # then it is incorrectly used. + final_tx_hash = cast( + SynchronizedData, self.synchronized_data + ).to_be_validated_tx_hash + + # We only set the final tx hash if we are about to exit from the transaction settlement skill. + # Then, the skills which use the transaction settlement can check the tx hash + # and if it is None, then it means that the transaction has failed. + synchronized_data = self.synchronized_data.update( + synchronized_data_class=self.synchronized_data_class, + **{ + self.collection_key: self.serialized_collection, + get_name( + SynchronizedData.final_verification_status + ): VerificationStatus.VERIFIED.value, + get_name(SynchronizedData.final_tx_hash): final_tx_hash, + }, + ) + return synchronized_data, self.done_event + if self.negative_vote_threshold_reached: + return self.synchronized_data, self.negative_event + if self.none_vote_threshold_reached: + return self.synchronized_data, self.none_event + if not self.is_majority_possible( + self.collection, self.synchronized_data.nb_participants + ): + return self.synchronized_data, self.no_majority_event + return None + + +class CheckTransactionHistoryRound(CollectSameUntilThresholdRound): + """A round in which agents check the transaction history to see if any previous tx has been validated""" + + payload_class = CheckTransactionHistoryPayload + synchronized_data_class = SynchronizedData + collection_key = get_name(SynchronizedData.participant_to_check) + selection_key = get_name(SynchronizedData.most_voted_check_result) + + def end_block( # pylint: disable=too-many-return-statements + self, + ) -> Optional[Tuple[BaseSynchronizedData, Enum]]: + """Process the end of the block.""" + if self.threshold_reached: + return_status, return_tx_hash = tx_hist_hex_to_payload( + self.most_voted_payload + ) + + if return_status == VerificationStatus.NOT_VERIFIED: + # We don't update the synchronized_data as we need to repeat all checks again later + synchronized_data = self.synchronized_data + else: + # We only set the final tx hash if we are about to exit from the transaction settlement skill. + # Then, the skills which use the transaction settlement can check the tx hash + # and if it is None, then it means that the transaction has failed. + synchronized_data = self.synchronized_data.update( + synchronized_data_class=self.synchronized_data_class, + **{ + self.collection_key: self.serialized_collection, + self.selection_key: self.most_voted_payload, + get_name( + SynchronizedData.final_verification_status + ): return_status.value, + get_name(SynchronizedData.final_tx_hash): return_tx_hash, + }, + ) + + if return_status == VerificationStatus.VERIFIED: + return synchronized_data, Event.DONE + if ( + return_status == VerificationStatus.NOT_VERIFIED + and cast( + SynchronizedData, self.synchronized_data + ).should_check_late_messages + ): + return synchronized_data, Event.CHECK_LATE_ARRIVING_MESSAGE + if return_status == VerificationStatus.NOT_VERIFIED: + return synchronized_data, Event.NEGATIVE + if return_status == VerificationStatus.BAD_SAFE_NONCE: + # in case a bad nonce was used, we need to recreate the tx from scratch + return synchronized_data, Event.NONE + + return synchronized_data, Event.NONE + + if not self.is_majority_possible( + self.collection, self.synchronized_data.nb_participants + ): + return self.synchronized_data, Event.NO_MAJORITY + return None + + +class CheckLateTxHashesRound(CheckTransactionHistoryRound): + """A round in which agents check the late-arriving transaction hashes to see if any of them has been validated""" + + +class SynchronizeLateMessagesRound(CollectNonEmptyUntilThresholdRound): + """A round in which agents synchronize potentially late arriving messages""" + + payload_class = SynchronizeLateMessagesPayload + synchronized_data_class = SynchronizedData + done_event = Event.DONE + none_event = Event.NONE + required_block_confirmations = 3 + selection_key = get_name(SynchronizedData.late_arriving_tx_hashes) + collection_key = get_name(SynchronizedData.participant_to_late_messages) + # if the payload is serialized to bytes, we verify that the length specified matches + _hash_length = TX_HASH_LENGTH + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: + """Process the end of the block.""" + result = super().end_block() + if result is None: + return None + + synchronized_data, event = cast(Tuple[SynchronizedData, Event], result) + + late_arriving_tx_hashes_counts = { + sender: len(hashes) + for sender, hashes in synchronized_data.late_arriving_tx_hashes.items() + } + missed_after_sync = { + sender: missed - late_arriving_tx_hashes_counts.get(sender, 0) + for sender, missed in synchronized_data.missed_messages.items() + } + suspects = tuple(filter_negative(missed_after_sync)) + + if suspects: + synchronized_data = cast( + SynchronizedData, + synchronized_data.update( + synchronized_data_class=self.synchronized_data_class, + **{get_name(SynchronizedData.suspects): suspects}, + ), + ) + return synchronized_data, Event.SUSPICIOUS_ACTIVITY + + synchronized_data = cast( + SynchronizedData, + synchronized_data.update( + synchronized_data_class=self.synchronized_data_class, + **{get_name(SynchronizedData.missed_messages): missed_after_sync}, + ), + ) + return synchronized_data, event + + def process_payload(self, payload: BaseTxPayload) -> None: + """Process payload.""" + # TODO: move check into payload definition via `post_init` + payload = cast(SynchronizeLateMessagesPayload, payload) + if self._hash_length: + content = payload.tx_hashes + if not content or len(content) % self._hash_length: + msg = f"Expecting serialized data of chunk size {self._hash_length}" + raise ABCIAppInternalError(f"{msg}, got: {content} in {self.round_id}") + super().process_payload(payload) + + def check_payload(self, payload: BaseTxPayload) -> None: + """Check Payload""" + # TODO: move check into payload definition via `post_init` + payload = cast(SynchronizeLateMessagesPayload, payload) + if self._hash_length: + content = payload.tx_hashes + if not content or len(content) % self._hash_length: + msg = f"Expecting serialized data of chunk size {self._hash_length}" + raise TransactionNotValidError( + f"{msg}, got: {content} in {self.round_id}" + ) + super().check_payload(payload) + + +class FinishedTransactionSubmissionRound(DegenerateRound, ABC): + """A round that represents the transition to the ResetAndPauseRound""" + + +class ResetRound(CollectSameUntilThresholdRound): + """A round that represents the reset of a period""" + + payload_class = ResetPayload + synchronized_data_class = SynchronizedData + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: + """Process the end of the block.""" + if self.threshold_reached: + synchronized_data = cast(SynchronizedData, self.synchronized_data) + # we could have used the `synchronized_data.create()` here and set the `cross_period_persisted_keys` + # with the corresponding properties' keys. However, the cross period keys would get passed over + # for all the following periods, even those that the tx settlement succeeds. + # Therefore, we need to manually call the db's create method and pass the keys we want to keep only + # for the next period, which comes after a `NO_MAJORITY` event of the tx settlement skill. + # TODO investigate the following: + # This probably indicates an issue with the logic of this skill. We should not increase the period since + # we have a failure. We could instead just remove the `ResetRound` and transition to the + # `RandomnessTransactionSubmissionRound` directly. This would save us one round, would allow us to remove + # this hacky logic for the `create`, and would also not increase the period count in non-successful events + self.synchronized_data.db.create( + **{ + db_key: synchronized_data.db.get(db_key, default) + for db_key, default in { + "all_participants": VALUE_NOT_PROVIDED, + "participants": VALUE_NOT_PROVIDED, + "consensus_threshold": VALUE_NOT_PROVIDED, + "safe_contract_address": VALUE_NOT_PROVIDED, + "tx_hashes_history": "", + "keepers": VALUE_NOT_PROVIDED, + "missed_messages": dict.fromkeys( + synchronized_data.all_participants, 0 + ), + "late_arriving_tx_hashes": VALUE_NOT_PROVIDED, + "suspects": tuple(), + }.items() + } + ) + return self.synchronized_data, Event.DONE + if not self.is_majority_possible( + self.collection, self.synchronized_data.nb_participants + ): + return self.synchronized_data, Event.NO_MAJORITY + return None + + +class TransactionSubmissionAbciApp(AbciApp[Event]): + """TransactionSubmissionAbciApp + + Initial round: RandomnessTransactionSubmissionRound + + Initial states: {RandomnessTransactionSubmissionRound} + + Transition states: + 0. RandomnessTransactionSubmissionRound + - done: 1. + - round timeout: 0. + - no majority: 0. + 1. SelectKeeperTransactionSubmissionARound + - done: 2. + - round timeout: 1. + - no majority: 10. + - incorrect serialization: 12. + 2. CollectSignatureRound + - done: 3. + - round timeout: 2. + - no majority: 10. + 3. FinalizationRound + - done: 4. + - check history: 5. + - finalize timeout: 7. + - finalization failed: 6. + - check late arriving message: 8. + - insufficient funds: 6. + 4. ValidateTransactionRound + - done: 11. + - negative: 5. + - none: 6. + - validate timeout: 5. + - no majority: 4. + 5. CheckTransactionHistoryRound + - done: 11. + - negative: 6. + - none: 12. + - check timeout: 5. + - no majority: 5. + - check late arriving message: 8. + 6. SelectKeeperTransactionSubmissionBRound + - done: 3. + - round timeout: 6. + - no majority: 10. + - incorrect serialization: 12. + 7. SelectKeeperTransactionSubmissionBAfterTimeoutRound + - done: 3. + - check history: 5. + - check late arriving message: 8. + - round timeout: 7. + - no majority: 10. + - incorrect serialization: 12. + 8. SynchronizeLateMessagesRound + - done: 9. + - round timeout: 8. + - none: 6. + - suspicious activity: 12. + 9. CheckLateTxHashesRound + - done: 11. + - negative: 12. + - none: 12. + - check timeout: 9. + - no majority: 12. + - check late arriving message: 8. + 10. ResetRound + - done: 0. + - reset timeout: 12. + - no majority: 12. + 11. FinishedTransactionSubmissionRound + 12. FailedRound + + Final states: {FailedRound, FinishedTransactionSubmissionRound} + + Timeouts: + round timeout: 30.0 + finalize timeout: 30.0 + validate timeout: 30.0 + check timeout: 30.0 + reset timeout: 30.0 + """ + + initial_round_cls: AppState = RandomnessTransactionSubmissionRound + initial_states: Set[AppState] = {RandomnessTransactionSubmissionRound} + transition_function: AbciAppTransitionFunction = { + RandomnessTransactionSubmissionRound: { + Event.DONE: SelectKeeperTransactionSubmissionARound, + Event.ROUND_TIMEOUT: RandomnessTransactionSubmissionRound, + Event.NO_MAJORITY: RandomnessTransactionSubmissionRound, + }, + SelectKeeperTransactionSubmissionARound: { + Event.DONE: CollectSignatureRound, + Event.ROUND_TIMEOUT: SelectKeeperTransactionSubmissionARound, + Event.NO_MAJORITY: ResetRound, + Event.INCORRECT_SERIALIZATION: FailedRound, + }, + CollectSignatureRound: { + Event.DONE: FinalizationRound, + Event.ROUND_TIMEOUT: CollectSignatureRound, + Event.NO_MAJORITY: ResetRound, + }, + FinalizationRound: { + Event.DONE: ValidateTransactionRound, + Event.CHECK_HISTORY: CheckTransactionHistoryRound, + Event.FINALIZE_TIMEOUT: SelectKeeperTransactionSubmissionBAfterTimeoutRound, + Event.FINALIZATION_FAILED: SelectKeeperTransactionSubmissionBRound, + Event.CHECK_LATE_ARRIVING_MESSAGE: SynchronizeLateMessagesRound, + Event.INSUFFICIENT_FUNDS: SelectKeeperTransactionSubmissionBRound, + }, + ValidateTransactionRound: { + Event.DONE: FinishedTransactionSubmissionRound, + Event.NEGATIVE: CheckTransactionHistoryRound, + Event.NONE: SelectKeeperTransactionSubmissionBRound, + # even in case of timeout we might've sent the transaction + # so we need to check the history + Event.VALIDATE_TIMEOUT: CheckTransactionHistoryRound, + Event.NO_MAJORITY: ValidateTransactionRound, + }, + CheckTransactionHistoryRound: { + Event.DONE: FinishedTransactionSubmissionRound, + Event.NEGATIVE: SelectKeeperTransactionSubmissionBRound, + Event.NONE: FailedRound, + Event.CHECK_TIMEOUT: CheckTransactionHistoryRound, + Event.NO_MAJORITY: CheckTransactionHistoryRound, + Event.CHECK_LATE_ARRIVING_MESSAGE: SynchronizeLateMessagesRound, + }, + SelectKeeperTransactionSubmissionBRound: { + Event.DONE: FinalizationRound, + Event.ROUND_TIMEOUT: SelectKeeperTransactionSubmissionBRound, + Event.NO_MAJORITY: ResetRound, + Event.INCORRECT_SERIALIZATION: FailedRound, + }, + SelectKeeperTransactionSubmissionBAfterTimeoutRound: { + Event.DONE: FinalizationRound, + Event.CHECK_HISTORY: CheckTransactionHistoryRound, + Event.CHECK_LATE_ARRIVING_MESSAGE: SynchronizeLateMessagesRound, + Event.ROUND_TIMEOUT: SelectKeeperTransactionSubmissionBAfterTimeoutRound, + Event.NO_MAJORITY: ResetRound, + Event.INCORRECT_SERIALIZATION: FailedRound, + }, + SynchronizeLateMessagesRound: { + Event.DONE: CheckLateTxHashesRound, + Event.ROUND_TIMEOUT: SynchronizeLateMessagesRound, + Event.NONE: SelectKeeperTransactionSubmissionBRound, + Event.SUSPICIOUS_ACTIVITY: FailedRound, + }, + CheckLateTxHashesRound: { + Event.DONE: FinishedTransactionSubmissionRound, + Event.NEGATIVE: FailedRound, + Event.NONE: FailedRound, + Event.CHECK_TIMEOUT: CheckLateTxHashesRound, + Event.NO_MAJORITY: FailedRound, + Event.CHECK_LATE_ARRIVING_MESSAGE: SynchronizeLateMessagesRound, + }, + ResetRound: { + Event.DONE: RandomnessTransactionSubmissionRound, + Event.RESET_TIMEOUT: FailedRound, + Event.NO_MAJORITY: FailedRound, + }, + FinishedTransactionSubmissionRound: {}, + FailedRound: {}, + } + final_states: Set[AppState] = { + FinishedTransactionSubmissionRound, + FailedRound, + } + event_to_timeout: Dict[Event, float] = { + Event.ROUND_TIMEOUT: 30.0, + Event.FINALIZE_TIMEOUT: 30.0, + Event.VALIDATE_TIMEOUT: 30.0, + Event.CHECK_TIMEOUT: 30.0, + Event.RESET_TIMEOUT: 30.0, + } + db_pre_conditions: Dict[AppState, Set[str]] = { + RandomnessTransactionSubmissionRound: { + get_name(SynchronizedData.most_voted_tx_hash), + get_name(SynchronizedData.participants), + } + } + db_post_conditions: Dict[AppState, Set[str]] = { + FinishedTransactionSubmissionRound: { + get_name(SynchronizedData.final_tx_hash), + get_name(SynchronizedData.final_verification_status), + }, + FailedRound: set(), + } diff --git a/packages/valory/skills/transaction_settlement_abci/skill.yaml b/packages/valory/skills/transaction_settlement_abci/skill.yaml new file mode 100644 index 0000000..61a9fab --- /dev/null +++ b/packages/valory/skills/transaction_settlement_abci/skill.yaml @@ -0,0 +1,174 @@ +name: transaction_settlement_abci +author: valory +version: 0.1.0 +type: skill +description: ABCI application for transaction settlement. +license: Apache-2.0 +aea_version: '>=1.0.0, <2.0.0' +fingerprint: + README.md: bafybeihvqvbj2tiiyimz3e27gqhb7ku5rut7hycfahi4qle732kvj5fs7q + __init__.py: bafybeicyrp6x2efg43gfdekxuofrlidc3w6aubzmyioqwnryropp6u7sby + behaviours.py: bafybeibv5y34bwaloj455bjws3a2aeh2bgi4dclq2f5d35apm2mloen5xa + dialogues.py: bafybeigabhaykiyzbluu4mk6bbrmqhzld2kyp32pg24bvjmzrrb74einwm + fsm_specification.yaml: bafybeigdj64py4zjihcxdkvtrydbxyeh4slr2kkghltz3upnupdgad4et4 + handlers.py: bafybeie42qa3csgy6oompuqs2qnkat5mnslepbbwmgoxv6ljme4jofa5pe + models.py: bafybeiguxishqvtvlyznok3xjnzm4t6vfflamcvz5vtecq5esbldsxuc5e + payload_tools.py: bafybeiatlbw3vyo5ppjhxf4psdvkwubmrjolsprf44lis5ozfkjo7o3cba + payloads.py: bafybeiclhjnsgylqzfnu2azlqxor3vyldaoof757dnfwz5xbwejk2ro2cm + rounds.py: bafybeieo5l6gh276hhtztphloyknb5ew66hvqhzzjiv26isaz7ptvtqjgu + test_tools/__init__.py: bafybeibj2blgxzvcgdi5gzcnlzs2nt7bpdifzvjjlxlrkeutjy2qrqbwau + test_tools/integration.py: bafybeictb7ym4xsbo3ti5y2a2fpg344graa4d7352oozsea5rbab3kq4ae + tests/__init__.py: bafybeifukcwmf2ewkjqdu7j6xzmaovgrul7jnea5lrl4o3ianoofje6vfa + tests/test_behaviours.py: bafybeia2vob5legv3tdrdj4gjgmnz6enhaterbetkc6ntdemnwgg5or4gq + tests/test_dialogues.py: bafybeictrjf6jzsj4y6u2ftdrb2nyriiipia5b7wc4fsli3lwbjpd3mbam + tests/test_handlers.py: bafybeievntkwacpfaom3qabvrlworjqyd4sgfjknjlhys7f5tuq7725xli + tests/test_models.py: bafybeihvrv7vtaei64nv7okkfz2gg2g4ey4nei27ayc74h5bdlqpbk4xde + tests/test_payload_tools.py: bafybeihmgkcrlqhz4ncak276lnccmilig6gx3crmn33n46jcco6g5pzrje + tests/test_payloads.py: bafybeidvjqvjvnuw5vt4zgnqwzopvprznmefosqy3wcxukvobaiishygze + tests/test_rounds.py: bafybeic3kzqy3pe6d4skntnfc5443y6dshcustiuv2d6cw4z56gw2ewehy + tests/test_tools/__init__.py: bafybeiaq2ftmklvu5vqq6vdfa7mrlmrnusluki35jm5n2yzf57ox5dif74 + tests/test_tools/test_integration.py: bafybeigv6fxogm3aq3extahr75owdqnzepouv3rtxl3m4gai2urtz6u4ea +fingerprint_ignore_patterns: [] +connections: [] +contracts: +- valory/gnosis_safe:0.1.0:bafybeiho6sbfts3zk3mftrngw37d5qnlvkqtnttt3fzexmcwkeevhu4wwi +protocols: +- open_aea/signing:1.0.0:bafybeihv62fim3wl2bayavfcg3u5e5cxu3b7brtu4cn5xoxd6lqwachasi +- valory/abci:0.1.0:bafybeiaqmp7kocbfdboksayeqhkbrynvlfzsx4uy4x6nohywnmaig4an7u +- valory/contract_api:1.0.0:bafybeidgu7o5llh26xp3u3ebq3yluull5lupiyeu6iooi2xyymdrgnzq5i +- valory/ledger_api:1.0.0:bafybeihdk6psr4guxmbcrc26jr2cbgzpd5aljkqvpwo64bvaz7tdti2oni +skills: +- valory/abstract_round_abci:0.1.0:bafybeibovsktd3uxur45nrcomq5shcn46cgxd5idmhxbmjhg32c5abyqim +behaviours: + main: + args: {} + class_name: TransactionSettlementRoundBehaviour +handlers: + abci: + args: {} + class_name: ABCIHandler + contract_api: + args: {} + class_name: ContractApiHandler + http: + args: {} + class_name: HttpHandler + ipfs: + args: {} + class_name: IpfsHandler + ledger_api: + args: {} + class_name: LedgerApiHandler + signing: + args: {} + class_name: SigningHandler + tendermint: + args: {} + class_name: TendermintHandler +models: + abci_dialogues: + args: {} + class_name: AbciDialogues + benchmark_tool: + args: + log_dir: /logs + class_name: BenchmarkTool + contract_api_dialogues: + args: {} + class_name: ContractApiDialogues + http_dialogues: + args: {} + class_name: HttpDialogues + ipfs_dialogues: + args: {} + class_name: IpfsDialogues + ledger_api_dialogues: + args: {} + class_name: LedgerApiDialogues + params: + args: + cleanup_history_depth: 1 + cleanup_history_depth_current: null + default_chain_id: ethereum + drand_public_key: 868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31 + finalize_timeout: 60.0 + genesis_config: + genesis_time: '2022-05-20T16:00:21.735122717Z' + chain_id: chain-c4daS1 + consensus_params: + block: + max_bytes: '22020096' + max_gas: '-1' + time_iota_ms: '1000' + evidence: + max_age_num_blocks: '100000' + max_age_duration: '172800000000000' + max_bytes: '1048576' + validator: + pub_key_types: + - ed25519 + version: {} + voting_power: '10' + history_check_timeout: 1205 + init_fallback_gas: 0 + keeper_allowed_retries: 3 + keeper_timeout: 30.0 + light_slash_unit_amount: 5000000000000000 + max_attempts: 10 + max_healthcheck: 120 + on_chain_service_id: null + request_retry_delay: 1.0 + request_timeout: 10.0 + reset_pause_duration: 10 + reset_tendermint_after: 2 + retry_attempts: 400 + retry_timeout: 3 + round_timeout_seconds: 30.0 + serious_slash_unit_amount: 8000000000000000 + service_id: registration + service_registry_address: null + setup: {} + share_tm_config_on_startup: false + slash_cooldown_hours: 3 + slash_threshold_amount: 10000000000000000 + sleep_time: 1 + tendermint_check_sleep_delay: 3 + tendermint_com_url: http://localhost:8080 + tendermint_max_retries: 5 + tendermint_p2p_url: localhost:26656 + tendermint_url: http://localhost:26657 + tx_timeout: 10.0 + use_slashing: false + use_termination: false + validate_timeout: 1205 + class_name: TransactionParams + randomness_api: + args: + api_id: cloudflare + headers: {} + method: GET + parameters: {} + response_key: null + response_type: dict + retries: 5 + url: https://drand.cloudflare.com/public/latest + class_name: RandomnessApi + requests: + args: {} + class_name: Requests + signing_dialogues: + args: {} + class_name: SigningDialogues + state: + args: {} + class_name: SharedState + tendermint_dialogues: + args: {} + class_name: TendermintDialogues +dependencies: + open-aea-test-autonomy: + version: ==0.15.2 + web3: + version: <7,>=6.0.0 +is_abstract: true +customs: [] diff --git a/packages/valory/skills/transaction_settlement_abci/test_tools/__init__.py b/packages/valory/skills/transaction_settlement_abci/test_tools/__init__.py new file mode 100644 index 0000000..55be4e3 --- /dev/null +++ b/packages/valory/skills/transaction_settlement_abci/test_tools/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Tests package for transaction_settlement_abci derived skills.""" diff --git a/packages/valory/skills/transaction_settlement_abci/test_tools/integration.py b/packages/valory/skills/transaction_settlement_abci/test_tools/integration.py new file mode 100644 index 0000000..deb1c53 --- /dev/null +++ b/packages/valory/skills/transaction_settlement_abci/test_tools/integration.py @@ -0,0 +1,338 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Integration tests for various transaction settlement skill's failure modes.""" + + +import binascii +import os +import tempfile +from abc import ABC +from math import ceil +from typing import Any, Dict, Union, cast + +from aea.crypto.base import Crypto +from aea.crypto.registries import make_crypto, make_ledger_api +from aea_ledger_ethereum import EthereumApi +from aea_test_autonomy.helpers.contracts import get_register_contract +from web3.types import Nonce, Wei + +from packages.open_aea.protocols.signing import SigningMessage +from packages.valory.contracts.gnosis_safe.tests.test_contract import ( + PACKAGE_DIR as GNOSIS_SAFE_PACKAGE, +) +from packages.valory.protocols.contract_api import ContractApiMessage +from packages.valory.protocols.contract_api.custom_types import RawTransaction, State +from packages.valory.protocols.ledger_api import LedgerApiMessage +from packages.valory.protocols.ledger_api.custom_types import ( + SignedTransaction, + TransactionDigest, + TransactionReceipt, +) +from packages.valory.skills.abstract_round_abci.test_tools.integration import ( + ExpectedContentType, + ExpectedTypesType, + HandlersType, + IntegrationBaseCase, +) +from packages.valory.skills.transaction_settlement_abci.behaviours import ( + FinalizeBehaviour, + ValidateTransactionBehaviour, +) +from packages.valory.skills.transaction_settlement_abci.payload_tools import ( + VerificationStatus, + skill_input_hex_to_payload, +) +from packages.valory.skills.transaction_settlement_abci.payloads import SignaturePayload +from packages.valory.skills.transaction_settlement_abci.rounds import ( + SynchronizedData as TxSettlementSynchronizedSata, +) + + +# pylint: disable=protected-access,too-many-ancestors,unbalanced-tuple-unpacking,too-many-locals,consider-using-with,unspecified-encoding,too-many-arguments,unidiomatic-typecheck + + +DUMMY_MAX_FEE_PER_GAS = 4000000000 +DUMMY_MAX_PRIORITY_FEE_PER_GAS = 3000000000 +DUMMY_REPRICING_MULTIPLIER = 1.1 + + +class _SafeConfiguredHelperIntegration(IntegrationBaseCase, ABC): # pragma: no cover + """Base test class for integration tests with Gnosis, but no contract, deployed.""" + + safe_owners: Dict[str, Crypto] + keeper_address: str + + @classmethod + def setup_class(cls, **kwargs: Any) -> None: + """Setup.""" + super().setup_class() + + # safe configuration + cls.safe_owners = {} + for address, p_key in cls.agents.items(): + with tempfile.TemporaryDirectory() as temp_dir: + fp = os.path.join(temp_dir, "key.txt") + f = open(fp, "w") + f.write(p_key) + f.close() + crypto = make_crypto("ethereum", private_key_path=str(fp)) + cls.safe_owners[address] = crypto + cls.keeper_address = cls.current_agent + assert cls.keeper_address in cls.safe_owners # nosec + + +class _GnosisHelperIntegration( + _SafeConfiguredHelperIntegration, ABC +): # pragma: no cover + """Class that assists Gnosis instantiation.""" + + safe_contract_address: str = "0x68FCdF52066CcE5612827E872c45767E5a1f6551" + ethereum_api: EthereumApi + gnosis_instance: Any + + @classmethod + def setup_class(cls, **kwargs: Any) -> None: + """Setup.""" + super().setup_class() + + # register gnosis contract + gnosis = get_register_contract(GNOSIS_SAFE_PACKAGE) + + cls.ethereum_api = make_ledger_api("ethereum") + cls.gnosis_instance = gnosis.get_instance( + cls.ethereum_api, cls.safe_contract_address + ) + + +class _TxHelperIntegration(_GnosisHelperIntegration, ABC): # pragma: no cover + """Class that assists tx settlement related operations.""" + + tx_settlement_synchronized_data: TxSettlementSynchronizedSata + + def sign_tx(self) -> None: + """Sign a transaction""" + tx_params = skill_input_hex_to_payload( + self.tx_settlement_synchronized_data.most_voted_tx_hash + ) + safe_tx_hash_bytes = binascii.unhexlify(tx_params["safe_tx_hash"]) + participant_to_signature = {} + for address, crypto in self.safe_owners.items(): + signature_hex = crypto.sign_message( + safe_tx_hash_bytes, + is_deprecated_mode=True, + ) + signature_hex = signature_hex[2:] + participant_to_signature[address] = SignaturePayload( + sender=address, + signature=signature_hex, + ).json + + # FIXME: The following loop is a patch. The + # [_get_python_modules](https://github.com/valory-xyz/open-aea/blob/d0e60881b1371442c3572df86c53fc92dc9228fa/aea/skills/base.py#L907-L925) + # is getting the python modules from the skill directory. + # As we can see from the code, the path will end up being relative, which means that the + # [_metaclass_registry_key](https://github.com/valory-xyz/open-autonomy/blob/5d151f1fff4934f70be8c5f6be77705cc2e6ef4c/packages/valory/skills/abstract_round_abci/base.py#L167) + # inserted in the `_MetaPayload`'s registry will also be relative. However, this is causing issues when calling + # `BaseTxPayload.from_json(payload_json)` later from the property below + # (`self.tx_settlement_synchronized_data.participant_to_signature`) because the `payload_json` will have been + # serialized using an imported payload (the `SignaturePayload` above), and therefore a key error will be + # raised since the imported payload's path is not relative and the registry has a relative path as a key. + for payload in participant_to_signature.values(): + registry_key = "_metaclass_registry_key" + payload_value = payload[registry_key] + payload_cls_name = payload_value.split(".")[-1] + patched_registry_key = f"payloads.{payload_cls_name}" + payload[registry_key] = patched_registry_key + + self.tx_settlement_synchronized_data.update( + participant_to_signature=participant_to_signature, + ) + + actual_safe_owners = self.gnosis_instance.functions.getOwners().call() + expected_safe_owners = ( + self.tx_settlement_synchronized_data.participant_to_signature.keys() + ) + assert len(actual_safe_owners) == len(expected_safe_owners) # nosec + assert all( # nosec + owner == signer + for owner, signer in zip(actual_safe_owners, expected_safe_owners) + ) + + def send_tx(self, simulate_timeout: bool = False) -> None: + """Send a transaction""" + + self.fast_forward_to_behaviour( + behaviour=self.behaviour, + behaviour_id=FinalizeBehaviour.auto_behaviour_id(), + synchronized_data=self.tx_settlement_synchronized_data, + ) + behaviour = cast(FinalizeBehaviour, self.behaviour.current_behaviour) + assert behaviour.behaviour_id == FinalizeBehaviour.auto_behaviour_id() + stored_nonce = behaviour.params.mutable_params.nonce + stored_gas_price = behaviour.params.mutable_params.gas_price + + handlers: HandlersType = [ + self.contract_handler, + self.signing_handler, + self.ledger_handler, + ] + expected_content: ExpectedContentType = [ + {"performative": ContractApiMessage.Performative.RAW_TRANSACTION}, + {"performative": SigningMessage.Performative.SIGNED_TRANSACTION}, + {"performative": LedgerApiMessage.Performative.TRANSACTION_DIGEST}, + ] + expected_types: ExpectedTypesType = [ + { + "raw_transaction": RawTransaction, + }, + { + "signed_transaction": SignedTransaction, + }, + { + "transaction_digest": TransactionDigest, + }, + ] + msg1, _, msg3 = self.process_n_messages( + 3, + self.tx_settlement_synchronized_data, + None, + handlers, + expected_content, + expected_types, + fail_send_a2a=simulate_timeout, + ) + assert msg1 is not None and isinstance(msg1, ContractApiMessage) # nosec + assert msg3 is not None and isinstance(msg3, LedgerApiMessage) # nosec + nonce_used = Nonce(int(cast(str, msg1.raw_transaction.body["nonce"]))) + gas_price_used = { + gas_price_param: Wei( + int( + cast( + str, + msg1.raw_transaction.body[gas_price_param], + ) + ) + ) + for gas_price_param in ("maxPriorityFeePerGas", "maxFeePerGas") + } + tx_digest = msg3.transaction_digest.body + tx_data = { + "status": VerificationStatus.PENDING, + "tx_digest": cast(str, tx_digest), + } + + behaviour = cast(FinalizeBehaviour, self.behaviour.current_behaviour) + assert behaviour.params.mutable_params.gas_price == gas_price_used # nosec + assert behaviour.params.mutable_params.nonce == nonce_used # nosec + if simulate_timeout: + assert behaviour.params.mutable_params.tx_hash == tx_digest # nosec + else: + assert behaviour.params.mutable_params.tx_hash == "" # nosec + + # if we are repricing + if nonce_used == stored_nonce: + assert stored_nonce is not None # nosec + assert stored_gas_price is not None # nosec + assert gas_price_used == { # nosec + gas_price_param: ceil( + stored_gas_price[gas_price_param] * DUMMY_REPRICING_MULTIPLIER + ) + for gas_price_param in ("maxPriorityFeePerGas", "maxFeePerGas") + }, "The repriced parameters do not match the ones returned from the gas pricing method!" + # if we are not repricing + else: + assert gas_price_used == { # nosec + "maxPriorityFeePerGas": DUMMY_MAX_PRIORITY_FEE_PER_GAS, + "maxFeePerGas": DUMMY_MAX_FEE_PER_GAS, + }, "The used parameters do not match the ones returned from the gas pricing method!" + + update_params: Dict[str, Union[int, str, Dict[str, int]]] + if not simulate_timeout: + hashes = self.tx_settlement_synchronized_data.tx_hashes_history + hashes.append(tx_digest) + update_params = dict( + tx_hashes_history="".join(hashes), + final_verification_status=VerificationStatus(tx_data["status"]).value, + ) + else: + # store the tx hash that we have missed and update missed messages. + assert isinstance( # nosec + self.behaviour.current_behaviour, FinalizeBehaviour + ) + self.mock_a2a_transaction() + self.behaviour.current_behaviour.params.mutable_params.tx_hash = tx_digest + missed_messages = self.tx_settlement_synchronized_data.missed_messages + missed_messages[ + self.tx_settlement_synchronized_data.most_voted_keeper_address + ] += 1 + update_params = dict(missed_messages=missed_messages) + + self.tx_settlement_synchronized_data.update( + synchronized_data_class=None, **update_params + ) + + def validate_tx( + self, simulate_timeout: bool = False, mining_interval_secs: float = 0 + ) -> None: + """Validate the sent transaction.""" + + if simulate_timeout: + missed_messages = self.tx_settlement_synchronized_data.missed_messages + missed_messages[ + tuple(self.tx_settlement_synchronized_data.all_participants)[0] + ] += 1 + self.tx_settlement_synchronized_data.update(missed_messages=missed_messages) + else: + handlers: HandlersType = [ + self.ledger_handler, + self.contract_handler, + ] + expected_content: ExpectedContentType = [ + {"performative": LedgerApiMessage.Performative.TRANSACTION_RECEIPT}, + {"performative": ContractApiMessage.Performative.STATE}, + ] + expected_types: ExpectedTypesType = [ + { + "transaction_receipt": TransactionReceipt, + }, + { + "state": State, + }, + ] + _, verif_msg = self.process_n_messages( + 2, + self.tx_settlement_synchronized_data, + ValidateTransactionBehaviour.auto_behaviour_id(), + handlers, + expected_content, + expected_types, + mining_interval_secs=mining_interval_secs, + ) + assert verif_msg is not None and isinstance( # nosec + verif_msg, ContractApiMessage + ) + assert verif_msg.state.body[ # nosec + "verified" + ], f"Message not verified: {verif_msg.state.body}" + + self.tx_settlement_synchronized_data.update( + final_verification_status=VerificationStatus.VERIFIED.value, + final_tx_hash=self.tx_settlement_synchronized_data.to_be_validated_tx_hash, + ) diff --git a/packages/valory/skills/transaction_settlement_abci/tests/__init__.py b/packages/valory/skills/transaction_settlement_abci/tests/__init__.py new file mode 100644 index 0000000..932ecb0 --- /dev/null +++ b/packages/valory/skills/transaction_settlement_abci/tests/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Tests for valory/transaction_settlement_abci skill.""" +from pathlib import Path + + +PACKAGE_DIR = Path(__file__).parents[1] diff --git a/packages/valory/skills/transaction_settlement_abci/tests/test_behaviours.py b/packages/valory/skills/transaction_settlement_abci/tests/test_behaviours.py new file mode 100644 index 0000000..fbc6d9f --- /dev/null +++ b/packages/valory/skills/transaction_settlement_abci/tests/test_behaviours.py @@ -0,0 +1,1392 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Tests for valory/registration_abci skill's behaviours.""" + +# pylint: skip-file + +import logging +import time +from collections import deque +from pathlib import Path +from typing import ( + Any, + Callable, + Deque, + Dict, + Generator, + List, + Optional, + Set, + Tuple, + Type, + Union, + cast, +) +from unittest import mock +from unittest.mock import MagicMock + +import pytest +from _pytest.logging import LogCaptureFixture +from _pytest.monkeypatch import MonkeyPatch +from aea.helpers.transaction.base import ( + RawTransaction, + SignedMessage, + SignedTransaction, +) +from aea.helpers.transaction.base import State as TrState +from aea.helpers.transaction.base import TransactionDigest, TransactionReceipt +from aea.skills.base import SkillContext +from web3.types import Nonce + +from packages.open_aea.protocols.signing import SigningMessage +from packages.valory.contracts.gnosis_safe.contract import ( + PUBLIC_ID as GNOSIS_SAFE_CONTRACT_ID, +) +from packages.valory.protocols.abci import AbciMessage # noqa: F401 +from packages.valory.protocols.contract_api.message import ContractApiMessage +from packages.valory.protocols.ledger_api.message import LedgerApiMessage +from packages.valory.skills.abstract_round_abci.base import AbciAppDB +from packages.valory.skills.abstract_round_abci.behaviour_utils import ( + BaseBehaviour, + RPCResponseStatus, + make_degenerate_behaviour, +) +from packages.valory.skills.abstract_round_abci.test_tools.base import ( + FSMBehaviourBaseCase, +) +from packages.valory.skills.abstract_round_abci.test_tools.common import ( + BaseRandomnessBehaviourTest, + BaseSelectKeeperBehaviourTest, +) +from packages.valory.skills.transaction_settlement_abci import PUBLIC_ID +from packages.valory.skills.transaction_settlement_abci.behaviours import ( + CheckLateTxHashesBehaviour, + CheckTransactionHistoryBehaviour, + FinalizeBehaviour, + REVERT_CODES_TO_REASONS, + RandomnessTransactionSubmissionBehaviour, + ResetBehaviour, + SelectKeeperTransactionSubmissionBehaviourA, + SelectKeeperTransactionSubmissionBehaviourB, + SignatureBehaviour, + SynchronizeLateMessagesBehaviour, + TransactionSettlementBaseBehaviour, + TxDataType, + ValidateTransactionBehaviour, +) +from packages.valory.skills.transaction_settlement_abci.payload_tools import ( + VerificationStatus, + hash_payload_to_hex, +) +from packages.valory.skills.transaction_settlement_abci.rounds import ( + Event as TransactionSettlementEvent, +) +from packages.valory.skills.transaction_settlement_abci.rounds import ( + FinishedTransactionSubmissionRound, +) +from packages.valory.skills.transaction_settlement_abci.rounds import ( + SynchronizedData as TransactionSettlementSynchronizedSata, +) + + +PACKAGE_DIR = Path(__file__).parent.parent + + +def mock_yield_and_return( + return_value: Any, +) -> Callable[[], Generator[None, None, Any]]: + """Wrapper for a Dummy generator that returns a `bool`.""" + + def yield_and_return(*_: Any, **__: Any) -> Generator[None, None, Any]: + """Dummy generator that returns a `bool`.""" + yield + return return_value + + return yield_and_return + + +def test_skill_public_id() -> None: + """Test skill module public ID""" + + assert PUBLIC_ID.name == Path(__file__).parents[1].name + assert PUBLIC_ID.author == Path(__file__).parents[3].name + + +class TransactionSettlementFSMBehaviourBaseCase(FSMBehaviourBaseCase): + """Base case for testing TransactionSettlement FSMBehaviour.""" + + path_to_skill = PACKAGE_DIR + + def ffw_signature(self, db_items: Optional[Dict] = None) -> None: + """Fast-forward to the `SignatureBehaviour`.""" + if db_items is None: + db_items = {} + + self.fast_forward_to_behaviour( + behaviour=self.behaviour, + behaviour_id=SignatureBehaviour.auto_behaviour_id(), + synchronized_data=TransactionSettlementSynchronizedSata( + AbciAppDB( + setup_data=AbciAppDB.data_to_lists(db_items), + ) + ), + ) + + +class TestTransactionSettlementBaseBehaviour(TransactionSettlementFSMBehaviourBaseCase): + """Test `TransactionSettlementBaseBehaviour`.""" + + @pytest.mark.parametrize( + "message, tx_digest, rpc_status, expected_data, replacement", + ( + ( + MagicMock( + performative=ContractApiMessage.Performative.ERROR, message="GS026" + ), + None, + RPCResponseStatus.SUCCESS, + { + "blacklisted_keepers": set(), + "keeper_retries": 2, + "keepers": deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35)), + "status": VerificationStatus.VERIFIED, + "tx_digest": "", + }, + False, + ), + ( + MagicMock( + performative=ContractApiMessage.Performative.ERROR, message="test" + ), + None, + RPCResponseStatus.SUCCESS, + { + "blacklisted_keepers": set(), + "keeper_retries": 2, + "keepers": deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35)), + "status": VerificationStatus.ERROR, + "tx_digest": "", + }, + False, + ), + ( + MagicMock(performative=ContractApiMessage.Performative.RAW_MESSAGE), + None, + RPCResponseStatus.SUCCESS, + { + "blacklisted_keepers": set(), + "keeper_retries": 2, + "keepers": deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35)), + "status": VerificationStatus.PENDING, + "tx_digest": "", + }, + False, + ), + ( + MagicMock(performative=ContractApiMessage.Performative.RAW_TRANSACTION), + None, + RPCResponseStatus.INCORRECT_NONCE, + { + "blacklisted_keepers": set(), + "keeper_retries": 2, + "keepers": deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35)), + "status": VerificationStatus.ERROR, + "tx_digest": "", + }, + False, + ), + ( + MagicMock(performative=ContractApiMessage.Performative.RAW_TRANSACTION), + None, + RPCResponseStatus.INSUFFICIENT_FUNDS, + { + "blacklisted_keepers": {"agent_1" + "-" * 35}, + "keeper_retries": 1, + "keepers": deque(("agent_3" + "-" * 35,)), + "status": VerificationStatus.INSUFFICIENT_FUNDS, + "tx_digest": "", + }, + False, + ), + ( + MagicMock(performative=ContractApiMessage.Performative.RAW_TRANSACTION), + None, + RPCResponseStatus.UNCLASSIFIED_ERROR, + { + "blacklisted_keepers": set(), + "keeper_retries": 2, + "keepers": deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35)), + "status": VerificationStatus.PENDING, + "tx_digest": "", + }, + False, + ), + ( + MagicMock(performative=ContractApiMessage.Performative.RAW_TRANSACTION), + None, + RPCResponseStatus.UNDERPRICED, + { + "blacklisted_keepers": set(), + "keeper_retries": 2, + "keepers": deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35)), + "status": VerificationStatus.PENDING, + "tx_digest": "", + }, + False, + ), + ( + MagicMock(performative=ContractApiMessage.Performative.RAW_TRANSACTION), + "test_digest_0", + RPCResponseStatus.ALREADY_KNOWN, + { + "blacklisted_keepers": set(), + "keeper_retries": 2, + "keepers": deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35)), + "status": VerificationStatus.PENDING, + "tx_digest": "test_digest_0", + }, + False, + ), + ( + MagicMock( + performative=ContractApiMessage.Performative.RAW_TRANSACTION, + raw_transaction=MagicMock( + body={ + "nonce": 0, + "maxPriorityFeePerGas": 10, + "maxFeePerGas": 20, + "gas": 0, + } + ), + ), + "test_digest_1", + RPCResponseStatus.SUCCESS, + { + "blacklisted_keepers": set(), + "keeper_retries": 2, + "keepers": deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35)), + "status": VerificationStatus.PENDING, + "tx_digest": "test_digest_1", + }, + False, + ), + ( + MagicMock( + performative=ContractApiMessage.Performative.RAW_TRANSACTION, + raw_transaction=MagicMock( + body={ + "nonce": 0, + "maxPriorityFeePerGas": 10, + "maxFeePerGas": 20, + "gas": 0, + } + ), + ), + "test_digest_2", + RPCResponseStatus.SUCCESS, + { + "blacklisted_keepers": set(), + "keeper_retries": 2, + "keepers": deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35)), + "status": VerificationStatus.PENDING, + "tx_digest": "test_digest_2", + }, + True, + ), + ), + ) + def test__get_tx_data( + self, + message: ContractApiMessage, + tx_digest: Optional[str], + rpc_status: RPCResponseStatus, + expected_data: TxDataType, + replacement: bool, + monkeypatch: MonkeyPatch, + ) -> None: + """Test `_get_tx_data`.""" + # fast-forward to any behaviour of the tx settlement skill + init_db_items = dict( + most_voted_tx_hash="b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d90000000" + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000002625a000x77E9b2EF921253A171Fa0CB9ba80558648Ff7215b0e6add595e00477c" + "f347d09797b156719dc5233283ac76e4efce2a674fe72d9b0e6add595e00477cf347d09797b156719dc5233283" + "ac76e4efce2a674fe72d9", + keepers=int(2).to_bytes(32, "big").hex() + + "".join(deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35))), + ) + self.ffw_signature(init_db_items) + behaviour = cast(SignatureBehaviour, self.behaviour.current_behaviour) + assert behaviour.behaviour_id == SignatureBehaviour.auto_behaviour_id() + # Set `nonce` to the same value as the returned, so that we test the tx replacement logging. + if replacement: + behaviour.params.mutable_params.nonce = Nonce(0) + + # patch the `send_raw_transaction` method + def dummy_send_raw_transaction( + *_: Any, **kwargs: Any + ) -> Generator[None, None, Tuple[Optional[str], RPCResponseStatus]]: + """Dummy `send_raw_transaction` method.""" + yield + return tx_digest, rpc_status + + monkeypatch.setattr( + BaseBehaviour, "send_raw_transaction", dummy_send_raw_transaction + ) + # call `_get_tx_data` + tx_data_iterator = cast( + TransactionSettlementBaseBehaviour, self.behaviour.current_behaviour + )._get_tx_data(message, use_flashbots=False) + + if message.performative == ContractApiMessage.Performative.RAW_TRANSACTION: + next(tx_data_iterator) + + try: + next(tx_data_iterator) + except StopIteration as res: + assert res.value == expected_data + + """Test the serialized_keepers method.""" + behaviour_ = self.behaviour.current_behaviour + assert behaviour_ is not None + assert behaviour_.serialized_keepers(deque([]), 1) == "" + assert ( + behaviour_.serialized_keepers(deque(["-" * 42]), 1) + == "0000000000000000000000000000000000000000000000000000000000000001" + "------------------------------------------" + ) + + @pytest.mark.parametrize( + argnames=["tx_body", "expected_params"], + argvalues=[ + [ + {"maxPriorityFeePerGas": "dummy", "maxFeePerGas": "dummy"}, + ["maxPriorityFeePerGas", "maxFeePerGas"], + ], + [{"gasPrice": "dummy"}, ["gasPrice"]], + [ + {"maxPriorityFeePerGas": "dummy"}, + [], + ], + [ + {"maxFeePerGas": "dummy"}, + [], + ], + [ + {}, + [], + ], + [ + { + "maxPriorityFeePerGas": "dummy", + "maxFeePerGas": "dummy", + "gasPrice": "dummy", + }, + ["maxPriorityFeePerGas", "maxFeePerGas"], + ], + ], + ) + def test_get_gas_price_params( + self, tx_body: dict, expected_params: List[str] + ) -> None: + """Test the get_gas_price_params method""" + # fast-forward to any behaviour of the tx settlement skill + self.ffw_signature() + + assert ( + cast( + TransactionSettlementBaseBehaviour, self.behaviour.current_behaviour + ).get_gas_price_params(tx_body) + == expected_params + ) + + def test_parse_revert_reason_successful(self) -> None: + """Test `_parse_revert_reason` method.""" + # fast-forward to any behaviour of the tx settlement skill + self.ffw_signature() + + for code, explanation in REVERT_CODES_TO_REASONS.items(): + message = MagicMock( + performative=ContractApiMessage.Performative.ERROR, + message=f"some text {code}.", + ) + + expected = f"Received a {code} revert error: {explanation}." + + assert ( + cast( + TransactionSettlementBaseBehaviour, self.behaviour.current_behaviour + )._parse_revert_reason(message) + == expected + ) + + @pytest.mark.parametrize( + "message", + ( + MagicMock( + performative=ContractApiMessage.Performative.ERROR, + message="Non existing code should be invalid GS086.", + ), + MagicMock( + performative=ContractApiMessage.Performative.ERROR, + message="Code not matching the regex should be invalid GS0265.", + ), + MagicMock( + performative=ContractApiMessage.Performative.ERROR, + message="No code in the message should be invalid.", + ), + MagicMock( + performative=ContractApiMessage.Performative.ERROR, + message="", # empty message should be invalid + ), + MagicMock( + performative=ContractApiMessage.Performative.ERROR, + message=None, # `None` message should be invalid + ), + ), + ) + def test_parse_revert_reason_unsuccessful( + self, message: ContractApiMessage + ) -> None: + """Test `_parse_revert_reason` method.""" + # fast-forward to any behaviour of the tx settlement skill + self.ffw_signature() + + expected = f"get_raw_safe_transaction unsuccessful! Received: {message}" + + assert ( + cast( + TransactionSettlementBaseBehaviour, self.behaviour.current_behaviour + )._parse_revert_reason(message) + == expected + ) + + +class TestRandomnessInOperation(BaseRandomnessBehaviourTest): + """Test randomness in operation.""" + + path_to_skill = PACKAGE_DIR + + randomness_behaviour_class = RandomnessTransactionSubmissionBehaviour + next_behaviour_class = SelectKeeperTransactionSubmissionBehaviourA + done_event = TransactionSettlementEvent.DONE + + +class TestSelectKeeperTransactionSubmissionBehaviourA(BaseSelectKeeperBehaviourTest): + """Test SelectKeeperBehaviour.""" + + path_to_skill = PACKAGE_DIR + + select_keeper_behaviour_class = SelectKeeperTransactionSubmissionBehaviourA + next_behaviour_class = SignatureBehaviour + done_event = TransactionSettlementEvent.DONE + _synchronized_data = TransactionSettlementSynchronizedSata + + +class TestSelectKeeperTransactionSubmissionBehaviourB( + TestSelectKeeperTransactionSubmissionBehaviourA +): + """Test SelectKeeperBehaviour.""" + + select_keeper_behaviour_class = SelectKeeperTransactionSubmissionBehaviourB + next_behaviour_class = FinalizeBehaviour + + @mock.patch.object( + TransactionSettlementSynchronizedSata, + "keepers", + new_callable=mock.PropertyMock, + ) + @mock.patch.object( + TransactionSettlementSynchronizedSata, + "keeper_retries", + new_callable=mock.PropertyMock, + ) + @mock.patch.object( + TransactionSettlementSynchronizedSata, + "final_verification_status", + new_callable=mock.PropertyMock, + ) + @pytest.mark.parametrize( + "keepers, keeper_retries, blacklisted_keepers, final_verification_status", + ( + ( + deque(f"keeper_{i}" for i in range(4)), + 1, + set(), + VerificationStatus.NOT_VERIFIED, + ), + (deque(("test_keeper",)), 2, set(), VerificationStatus.PENDING), + (deque(("test_keeper",)), 2, set(), VerificationStatus.NOT_VERIFIED), + (deque(("test_keeper",)), 2, {"a1"}, VerificationStatus.NOT_VERIFIED), + ( + deque(("test_keeper",)), + 2, + {"test_keeper"}, + VerificationStatus.NOT_VERIFIED, + ), + ( + deque(("test_keeper",)), + 2, + {"a_1", "a_2", "test_keeper"}, + VerificationStatus.NOT_VERIFIED, + ), + (deque(("test_keeper",)), 1, set(), VerificationStatus.NOT_VERIFIED), + (deque(("test_keeper",)), 3, set(), VerificationStatus.NOT_VERIFIED), + ), + ) + def test_select_keeper( + self, + final_verification_status_mock: mock.PropertyMock, + keeper_retries_mock: mock.PropertyMock, + keepers_mock: mock.PropertyMock, + keepers: Deque[str], + keeper_retries: int, + blacklisted_keepers: Set[str], + final_verification_status: VerificationStatus, + ) -> None: + """Test select keeper agent.""" + keepers_mock.return_value = keepers + keeper_retries_mock.return_value = keeper_retries + final_verification_status_mock.return_value = final_verification_status + super().test_select_keeper(blacklisted_keepers=blacklisted_keepers) + + @mock.patch.object( + TransactionSettlementSynchronizedSata, + "final_verification_status", + new_callable=mock.PropertyMock, + return_value=VerificationStatus.PENDING, + ) + @pytest.mark.skip # Needs to be investigated, fails in CI only. look at #1710 + def test_select_keeper_tx_pending( + self, _: mock.PropertyMock, caplog: LogCaptureFixture + ) -> None: + """Test select keeper while tx is pending""" + + with caplog.at_level(logging.INFO): + super().test_select_keeper(blacklisted_keepers=set()) + assert "Kept keepers and incremented retries" in caplog.text + + +class TestSignatureBehaviour(TransactionSettlementFSMBehaviourBaseCase): + """Test SignatureBehaviour.""" + + def test_signature_behaviour( + self, + ) -> None: + """Test signature behaviour.""" + + init_db_items = dict( + most_voted_tx_hash="b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d90000000" + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000002625a000x77E9b2EF921253A171Fa0CB9ba80558648Ff7215b0e6add595e00477c" + "f347d09797b156719dc5233283ac76e4efce2a674fe72d9b0e6add595e00477cf347d09797b156719dc5233283" + "ac76e4efce2a674fe72d9", + ) + self.ffw_signature(init_db_items) + + assert ( + cast( + BaseBehaviour, + cast(BaseBehaviour, self.behaviour.current_behaviour), + ).behaviour_id + == SignatureBehaviour.auto_behaviour_id() + ) + self.behaviour.act_wrapper() + self.mock_signing_request( + request_kwargs=dict( + performative=SigningMessage.Performative.SIGN_MESSAGE, + ), + response_kwargs=dict( + performative=SigningMessage.Performative.SIGNED_MESSAGE, + signed_message=SignedMessage( + ledger_id="ethereum", body="stub_signature" + ), + ), + ) + self.mock_a2a_transaction() + self._test_done_flag_set() + self.end_round(TransactionSettlementEvent.DONE) + behaviour = cast(BaseBehaviour, self.behaviour.current_behaviour) + assert behaviour.behaviour_id == FinalizeBehaviour.auto_behaviour_id() + + +class TestFinalizeBehaviour(TransactionSettlementFSMBehaviourBaseCase): + """Test FinalizeBehaviour.""" + + behaviour_class = FinalizeBehaviour + + def test_non_sender_act( + self, + ) -> None: + """Test finalize behaviour.""" + participants = (self.skill.skill_context.agent_address, "a_1", "a_2") + retries = 1 + self.fast_forward_to_behaviour( + behaviour=self.behaviour, + behaviour_id=self.behaviour_class.auto_behaviour_id(), + synchronized_data=TransactionSettlementSynchronizedSata( + AbciAppDB( + setup_data=AbciAppDB.data_to_lists( + dict( + most_voted_keeper_address="most_voted_keeper_address", + participants=participants, + # keeper needs to have length == 42 in order to be parsed + keepers=retries.to_bytes(32, "big").hex() + + "other_agent" + + "-" * 31, + ) + ), + ) + ), + ) + assert self.behaviour.current_behaviour is not None + assert ( + self.behaviour.current_behaviour.behaviour_id + == self.behaviour_class.auto_behaviour_id() + ) + cast( + FinalizeBehaviour, self.behaviour.current_behaviour + ).params.mutable_params.tx_hash = "test" + self.behaviour.act_wrapper() + self._test_done_flag_set() + self.end_round(TransactionSettlementEvent.DONE) + behaviour = cast(ValidateTransactionBehaviour, self.behaviour.current_behaviour) + assert ( + behaviour.behaviour_id == ValidateTransactionBehaviour.auto_behaviour_id() + ) + assert behaviour.params.mutable_params.tx_hash == "test" + + @pytest.mark.parametrize( + "resubmitting, response_kwargs", + ( + ( + ( + True, + dict( + performative=ContractApiMessage.Performative.RAW_TRANSACTION, + callable="get_deploy_transaction", + raw_transaction=RawTransaction( + ledger_id="ethereum", + body={ + "tx_hash": "0x3b", + "nonce": 0, + "maxFeePerGas": int(10e10), + "maxPriorityFeePerGas": int(10e10), + }, + ), + ), + ) + ), + ( + False, + dict( + performative=ContractApiMessage.Performative.RAW_TRANSACTION, + callable="get_deploy_transaction", + raw_transaction=RawTransaction( + ledger_id="ethereum", + body={ + "tx_hash": "0x3b", + "nonce": 0, + "maxFeePerGas": int(10e10), + "maxPriorityFeePerGas": int(10e10), + }, + ), + ), + ), + ( + False, + dict( + performative=ContractApiMessage.Performative.ERROR, + callable="get_deploy_transaction", + code=500, + message="GS026", + data=b"", + ), + ), + ( + False, + dict( + performative=ContractApiMessage.Performative.ERROR, + callable="get_deploy_transaction", + code=500, + message="other error", + data=b"", + ), + ), + ), + ) + @mock.patch.object(SkillContext, "agent_address", new_callable=mock.PropertyMock) + def test_sender_act( + self, + agent_address_mock: mock.PropertyMock, + resubmitting: bool, + response_kwargs: Dict[ + str, + Union[ + int, + str, + bytes, + Dict[str, Union[int, str]], + ContractApiMessage.Performative, + RawTransaction, + ], + ], + ) -> None: + """Test finalize behaviour.""" + nonce: Optional[int] = None + max_priority_fee_per_gas: Optional[int] = None + + if resubmitting: + nonce = 0 + max_priority_fee_per_gas = 1 + + # keepers need to have length == 42 in order to be parsed + agent_address_mock.return_value = "-" * 42 + retries = 1 + participants = ( + self.skill.skill_context.agent_address, + "a_1" + "-" * 39, + "a_2" + "-" * 39, + ) + self.fast_forward_to_behaviour( + behaviour=self.behaviour, + behaviour_id=self.behaviour_class.auto_behaviour_id(), + synchronized_data=TransactionSettlementSynchronizedSata( + AbciAppDB( + setup_data=AbciAppDB.data_to_lists( + dict( + safe_contract_address="safe_contract_address", + participants=participants, + participant_to_signature={}, + most_voted_tx_hash=hash_payload_to_hex( + "b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", + 1, + 1, + "0x77E9b2EF921253A171Fa0CB9ba80558648Ff7215", + b"b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", + ), + nonce=nonce, + max_priority_fee_per_gas=max_priority_fee_per_gas, + keepers=retries.to_bytes(32, "big").hex() + + self.skill.skill_context.agent_address, + ) + ), + ) + ), + ) + + assert self.behaviour.current_behaviour is not None + assert ( + self.behaviour.current_behaviour.behaviour_id + == self.behaviour_class.auto_behaviour_id() + ) + cast( + FinalizeBehaviour, self.behaviour.current_behaviour + ).params.mutable_params.tx_hash = "test" + self.behaviour.act_wrapper() + + self.mock_contract_api_request( + request_kwargs=dict( + performative=ContractApiMessage.Performative.GET_RAW_TRANSACTION, + ), + contract_id=str(GNOSIS_SAFE_CONTRACT_ID), + response_kwargs=response_kwargs, + ) + + if ( + response_kwargs["performative"] + == ContractApiMessage.Performative.RAW_TRANSACTION + ): + self.mock_signing_request( + request_kwargs=dict( + performative=SigningMessage.Performative.SIGN_TRANSACTION + ), + response_kwargs=dict( + performative=SigningMessage.Performative.SIGNED_TRANSACTION, + signed_transaction=SignedTransaction(ledger_id="ethereum", body={}), + ), + ) + self.mock_ledger_api_request( + request_kwargs=dict( + performative=LedgerApiMessage.Performative.SEND_SIGNED_TRANSACTION + ), + response_kwargs=dict( + performative=LedgerApiMessage.Performative.TRANSACTION_DIGEST, + transaction_digest=TransactionDigest( + ledger_id="ethereum", body="tx_hash" + ), + ), + ) + + self.mock_a2a_transaction() + self._test_done_flag_set() + self.end_round(TransactionSettlementEvent.DONE) + assert ( + self.behaviour.current_behaviour.behaviour_id + == ValidateTransactionBehaviour.auto_behaviour_id() + ) + assert ( + cast( + ValidateTransactionBehaviour, self.behaviour.current_behaviour + ).params.mutable_params.tx_hash + == "" + ) + + def test_sender_act_tx_data_contains_tx_digest(self) -> None: + """Test finalize behaviour.""" + + max_priority_fee_per_gas: Optional[int] = None + + retries = 1 + participants = ( + self.skill.skill_context.agent_address, + "a_1" + "-" * 39, + "a_2" + "-" * 39, + ) + kwargs = dict( + safe_contract_address="safe_contract_address", + participants=participants, + participant_to_signature={}, + most_voted_tx_hash=hash_payload_to_hex( + "b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", + 1, + 1, + "0x77E9b2EF921253A171Fa0CB9ba80558648Ff7215", + b"b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", + ), + nonce=None, + max_priority_fee_per_gas=max_priority_fee_per_gas, + keepers=retries.to_bytes(32, "big").hex() + + self.skill.skill_context.agent_address, + ) + + db = AbciAppDB(setup_data=AbciAppDB.data_to_lists(kwargs)) + + self.fast_forward_to_behaviour( + behaviour=self.behaviour, + behaviour_id=self.behaviour_class.auto_behaviour_id(), + synchronized_data=TransactionSettlementSynchronizedSata(db), + ) + + response_kwargs = dict( + performative=ContractApiMessage.Performative.RAW_TRANSACTION, + callable="get_deploy_transaction", + raw_transaction=RawTransaction( + ledger_id="ethereum", + body={ + "tx_hash": "0x3b", + "nonce": 0, + "maxFeePerGas": int(10e10), + "maxPriorityFeePerGas": int(10e10), + }, + ), + ) + + # mock the returned tx_data + return_value = dict( + status=VerificationStatus.PENDING, + keepers=deque(), + keeper_retries=1, + blacklisted_keepers=set(), + tx_digest="dummy_tx_digest", + ) + + current_behaviour = cast( + TransactionSettlementBaseBehaviour, self.behaviour.current_behaviour + ) + current_behaviour._get_tx_data = mock_yield_and_return(return_value) # type: ignore + + self.behaviour.act_wrapper() + self.mock_contract_api_request( + request_kwargs=dict( + performative=ContractApiMessage.Performative.GET_RAW_TRANSACTION, + ), + contract_id=str(GNOSIS_SAFE_CONTRACT_ID), + response_kwargs=response_kwargs, + ) + self.behaviour.act_wrapper() + self.mock_a2a_transaction() + self._test_done_flag_set() + self.end_round(TransactionSettlementEvent.DONE) + + current_behaviour = cast( + ValidateTransactionBehaviour, self.behaviour.current_behaviour + ) + current_behaviour_id = current_behaviour.behaviour_id + expected_behaviour_id = ValidateTransactionBehaviour.auto_behaviour_id() + assert current_behaviour_id == expected_behaviour_id + + def test_handle_late_messages(self) -> None: + """Test `handle_late_messages.`""" + participants = (self.skill.skill_context.agent_address, "a_1", "a_2") + self.fast_forward_to_behaviour( + behaviour=self.behaviour, + behaviour_id=self.behaviour_class.auto_behaviour_id(), + synchronized_data=TransactionSettlementSynchronizedSata( + AbciAppDB( + setup_data=AbciAppDB.data_to_lists( + dict( + most_voted_keeper_address="most_voted_keeper_address", + participants=participants, + keepers="keepers", + ) + ), + ) + ), + ) + self.behaviour.current_behaviour = cast( + BaseBehaviour, self.behaviour.current_behaviour + ) + assert ( + self.behaviour.current_behaviour.behaviour_id + == self.behaviour_class.auto_behaviour_id() + ) + + message = ContractApiMessage(ContractApiMessage.Performative.RAW_MESSAGE) # type: ignore + self.behaviour.current_behaviour.handle_late_messages( + self.behaviour.current_behaviour.behaviour_id, message + ) + assert cast( + TransactionSettlementBaseBehaviour, self.behaviour.current_behaviour + ).params.mutable_params.late_messages == [message] + + with mock.patch.object(self.behaviour.context.logger, "warning") as mock_info: + self.behaviour.current_behaviour.handle_late_messages( + "other_behaviour_id", message + ) + mock_info.assert_called_with( + f"No callback defined for request with nonce: {message.dialogue_reference[0]}, " + "arriving for behaviour: other_behaviour_id" + ) + message = MagicMock() + self.behaviour.current_behaviour.handle_late_messages( + self.behaviour.current_behaviour.behaviour_id, message + ) + mock_info.assert_called_with( + f"No callback defined for request with nonce: {message.dialogue_reference[0]}, " + f"arriving for behaviour: {FinalizeBehaviour.auto_behaviour_id()}" + ) + + +class TestValidateTransactionBehaviour(TransactionSettlementFSMBehaviourBaseCase): + """Test ValidateTransactionBehaviour.""" + + def _fast_forward(self) -> None: + """Fast-forward to relevant behaviour.""" + participants = (self.skill.skill_context.agent_address, "a_1", "a_2") + most_voted_keeper_address = self.skill.skill_context.agent_address + self.fast_forward_to_behaviour( + behaviour=self.behaviour, + behaviour_id=ValidateTransactionBehaviour.auto_behaviour_id(), + synchronized_data=TransactionSettlementSynchronizedSata( + AbciAppDB( + setup_data=AbciAppDB.data_to_lists( + dict( + safe_contract_address="safe_contract_address", + tx_hashes_history="t" * 66, + final_tx_hash="dummy_hash", + participants=participants, + most_voted_keeper_address=most_voted_keeper_address, + participant_to_signature={}, + most_voted_tx_hash=hash_payload_to_hex( + "b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", + 1, + 1, + "0x77E9b2EF921253A171Fa0CB9ba80558648Ff7215", + b"b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", + ), + max_priority_fee_per_gas=int(10e10), + ) + ), + ) + ), + ) + assert ( + cast( + BaseBehaviour, + cast(BaseBehaviour, self.behaviour.current_behaviour), + ).behaviour_id + == ValidateTransactionBehaviour.auto_behaviour_id() + ) + + def test_validate_transaction_safe_behaviour( + self, + ) -> None: + """Test ValidateTransactionBehaviour.""" + self._fast_forward() + self.behaviour.act_wrapper() + self.mock_ledger_api_request( + request_kwargs=dict( + performative=LedgerApiMessage.Performative.GET_TRANSACTION_RECEIPT + ), + response_kwargs=dict( + performative=LedgerApiMessage.Performative.TRANSACTION_RECEIPT, + transaction_receipt=TransactionReceipt( + ledger_id="ethereum", receipt={"status": 1}, transaction={} + ), + ), + ) + self.mock_contract_api_request( + request_kwargs=dict(performative=ContractApiMessage.Performative.GET_STATE), + contract_id=str(GNOSIS_SAFE_CONTRACT_ID), + response_kwargs=dict( + performative=ContractApiMessage.Performative.STATE, + callable="get_deploy_transaction", + state=TrState(ledger_id="ethereum", body={"verified": True}), + ), + ) + self.mock_a2a_transaction() + self._test_done_flag_set() + self.end_round(TransactionSettlementEvent.DONE) + behaviour = cast(BaseBehaviour, self.behaviour.current_behaviour) + assert ( + behaviour.behaviour_id + == make_degenerate_behaviour( + FinishedTransactionSubmissionRound + ).auto_behaviour_id() + ) + + def test_validate_transaction_safe_behaviour_no_tx_sent( + self, + ) -> None: + """Test ValidateTransactionBehaviour when tx cannot be sent.""" + self._fast_forward() + + with mock.patch.object(self.behaviour.context.logger, "error") as mock_logger: + self.behaviour.act_wrapper() + self.mock_ledger_api_request( + request_kwargs=dict( + performative=LedgerApiMessage.Performative.GET_TRANSACTION_RECEIPT, + ), + response_kwargs=dict( + performative=LedgerApiMessage.Performative.ERROR, + code=1, + ), + ) + behaviour = cast( + TransactionSettlementBaseBehaviour, + self.behaviour.current_behaviour, + ) + latest_tx_hash = behaviour.synchronized_data.tx_hashes_history[-1] + mock_logger.assert_any_call(f"tx {latest_tx_hash} receipt check timed out!") + + +class TestCheckTransactionHistoryBehaviour(TransactionSettlementFSMBehaviourBaseCase): + """Test CheckTransactionHistoryBehaviour.""" + + def _fast_forward(self, hashes_history: str) -> None: + """Fast-forward to relevant behaviour.""" + self.fast_forward_to_behaviour( + behaviour=self.behaviour, + behaviour_id=CheckTransactionHistoryBehaviour.auto_behaviour_id(), + synchronized_data=TransactionSettlementSynchronizedSata( + AbciAppDB( + setup_data=AbciAppDB.data_to_lists( + dict( + safe_contract_address="safe_contract_address", + participants=( + self.skill.skill_context.agent_address, + "a_1", + "a_2", + ), + participant_to_signature={}, + most_voted_tx_hash="b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002625a000x77E9b2EF921253A171Fa0CB9ba80558648Ff7215b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", + tx_hashes_history=hashes_history, + ) + ), + ) + ), + ) + assert ( + cast(BaseBehaviour, self.behaviour.current_behaviour).behaviour_id + == CheckTransactionHistoryBehaviour.auto_behaviour_id() + ) + + @pytest.mark.parametrize( + "verified, status, hashes_history, revert_reason", + ( + (False, -1, "0x" + "t" * 64, "test"), + (False, 0, "", "test"), + (False, 0, "0x" + "t" * 64, "test"), + (False, 0, "0x" + "t" * 64, "GS026"), + (True, 1, "0x" + "t" * 64, "test"), + ), + ) + def test_check_tx_history_behaviour( + self, + verified: bool, + status: int, + hashes_history: str, + revert_reason: str, + ) -> None: + """Test CheckTransactionHistoryBehaviour.""" + self._fast_forward(hashes_history) + self.behaviour.act_wrapper() + + if hashes_history: + self.mock_contract_api_request( + request_kwargs=dict( + performative=ContractApiMessage.Performative.GET_STATE + ), + contract_id=str(GNOSIS_SAFE_CONTRACT_ID), + response_kwargs=dict( + performative=ContractApiMessage.Performative.STATE, + callable="get_safe_nonce", + state=TrState( + ledger_id="ethereum", + body={ + "safe_nonce": 0, + }, + ), + ), + ) + self.mock_contract_api_request( + request_kwargs=dict( + performative=ContractApiMessage.Performative.GET_STATE + ), + contract_id=str(GNOSIS_SAFE_CONTRACT_ID), + response_kwargs=dict( + performative=ContractApiMessage.Performative.STATE, + callable="verify_tx", + state=TrState( + ledger_id="ethereum", + body={ + "verified": verified, + "status": status, + "transaction": {}, + }, + ), + ), + ) + + if not verified and status != -1: + self.mock_contract_api_request( + request_kwargs=dict( + performative=ContractApiMessage.Performative.GET_STATE + ), + contract_id=str(GNOSIS_SAFE_CONTRACT_ID), + response_kwargs=dict( + performative=ContractApiMessage.Performative.STATE, + callable="revert_reason", + state=TrState( + ledger_id="ethereum", body={"revert_reason": revert_reason} + ), + ), + ) + self.behaviour.act_wrapper() + self.mock_a2a_transaction() + self._test_done_flag_set() + self.end_round(TransactionSettlementEvent.DONE) + behaviour = cast(BaseBehaviour, self.behaviour.current_behaviour) + assert ( + behaviour.behaviour_id + == make_degenerate_behaviour( + FinishedTransactionSubmissionRound + ).auto_behaviour_id() + ) + + @pytest.mark.parametrize( + "verified, status, hashes_history, revert_reason", + ((False, 0, "0x" + "t" * 64, "test"),), + ) + def test_check_tx_history_behaviour_negative( + self, + verified: bool, + status: int, + hashes_history: str, + revert_reason: str, + ) -> None: + """Test CheckTransactionHistoryBehaviour.""" + self._fast_forward(hashes_history) + self.behaviour.act_wrapper() + self.behaviour.context.params.mutable_params.nonce = 1 + if hashes_history: + self.mock_contract_api_request( + request_kwargs=dict( + performative=ContractApiMessage.Performative.GET_STATE + ), + contract_id=str(GNOSIS_SAFE_CONTRACT_ID), + response_kwargs=dict( + performative=ContractApiMessage.Performative.STATE, + callable="get_safe_nonce", + state=TrState( + ledger_id="ethereum", + body={ + "safe_nonce": 1, + }, + ), + ), + ) + self.behaviour.act_wrapper() + self.mock_a2a_transaction() + self._test_done_flag_set() + self.end_round(TransactionSettlementEvent.DONE) + behaviour = cast(BaseBehaviour, self.behaviour.current_behaviour) + assert ( + behaviour.behaviour_id + == make_degenerate_behaviour( + FinishedTransactionSubmissionRound + ).auto_behaviour_id() + ) + + +class TestCheckLateTxHashesBehaviour(TransactionSettlementFSMBehaviourBaseCase): + """Test CheckLateTxHashesBehaviour.""" + + def _fast_forward(self, late_arriving_tx_hashes: Dict[str, str]) -> None: + """Fast-forward to relevant behaviour.""" + + agent_address = self.skill.skill_context.agent_address + kwargs = dict( + safe_contract_address="safe_contract_address", + participants=(agent_address, "a_1", "a_2"), + participant_to_signature={}, + most_voted_tx_hash="", + late_arriving_tx_hashes=late_arriving_tx_hashes, + ) + abci_app_db = AbciAppDB(setup_data=AbciAppDB.data_to_lists(kwargs)) + + self.fast_forward_to_behaviour( + behaviour=self.behaviour, + behaviour_id=CheckLateTxHashesBehaviour.auto_behaviour_id(), + synchronized_data=TransactionSettlementSynchronizedSata(abci_app_db), + ) + + current_behaviour = self.behaviour.current_behaviour + current_behaviour_id = cast(BaseBehaviour, current_behaviour).behaviour_id + assert current_behaviour_id == CheckLateTxHashesBehaviour.auto_behaviour_id() + + def test_check_tx_history_behaviour(self) -> None: + """Test CheckTransactionHistoryBehaviour.""" + self._fast_forward(late_arriving_tx_hashes={}) + self.behaviour.act_wrapper() + self.behaviour.act_wrapper() + self.mock_a2a_transaction() + self._test_done_flag_set() + self.end_round(TransactionSettlementEvent.DONE) + behaviour = cast(BaseBehaviour, self.behaviour.current_behaviour) + next_degen_behaviour = make_degenerate_behaviour( + FinishedTransactionSubmissionRound + ) + assert behaviour.behaviour_id == next_degen_behaviour.auto_behaviour_id() + + +class TestSynchronizeLateMessagesBehaviour(TransactionSettlementFSMBehaviourBaseCase): + """Test `SynchronizeLateMessagesBehaviour`""" + + def _check_behaviour_id( + self, expected: Type[TransactionSettlementBaseBehaviour] + ) -> None: + behaviour = cast(BaseBehaviour, self.behaviour.current_behaviour) + assert behaviour.behaviour_id == expected.auto_behaviour_id() + + @pytest.mark.parametrize("late_messages", ([], [MagicMock, MagicMock])) + def test_async_act(self, late_messages: List[MagicMock]) -> None: + """Test `async_act`""" + cast( + TransactionSettlementBaseBehaviour, self.behaviour.current_behaviour + ).params.mutable_params.late_messages = late_messages + + participants = (self.skill.skill_context.agent_address, "a_1", "a_2") + self.fast_forward_to_behaviour( + behaviour=self.behaviour, + behaviour_id=SynchronizeLateMessagesBehaviour.auto_behaviour_id(), + synchronized_data=TransactionSettlementSynchronizedSata( + AbciAppDB( + setup_data=dict( + participants=[participants], + participant_to_signature=[{}], + safe_contract_address=["safe_contract_address"], + most_voted_tx_hash=[ + hash_payload_to_hex( + "b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", + 1, + 1, + "0x77E9b2EF921253A171Fa0CB9ba80558648Ff7215", + b"b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9" + b"b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", + ) + ], + ), + ) + ), + ) + self._check_behaviour_id(SynchronizeLateMessagesBehaviour) # type: ignore + + if not late_messages: + self.behaviour.act_wrapper() + self.mock_a2a_transaction() + self._test_done_flag_set() + self.end_round(TransactionSettlementEvent.DONE) + self._check_behaviour_id(CheckLateTxHashesBehaviour) # type: ignore + + else: + + def _dummy_get_tx_data( + _current_message: ContractApiMessage, + _use_flashbots: bool, + chain_id: Optional[str] = None, + ) -> Generator[None, None, TxDataType]: + yield + return { + "status": VerificationStatus.PENDING, + "tx_digest": "test", + } + + cast( + TransactionSettlementBaseBehaviour, self.behaviour.current_behaviour + )._get_tx_data = _dummy_get_tx_data # type: ignore + for _ in range(len(late_messages)): + self.behaviour.act_wrapper() + + +class TestResetBehaviour(TransactionSettlementFSMBehaviourBaseCase): + """Test the reset behaviour.""" + + behaviour_class = ResetBehaviour + next_behaviour_class = RandomnessTransactionSubmissionBehaviour + + def test_reset_behaviour( + self, + ) -> None: + """Test reset behaviour.""" + self.fast_forward_to_behaviour( + behaviour=self.behaviour, + behaviour_id=self.behaviour_class.auto_behaviour_id(), + synchronized_data=TransactionSettlementSynchronizedSata( + AbciAppDB(setup_data=dict(estimate=[1.0])), + ), + ) + assert ( + cast( + BaseBehaviour, + cast(BaseBehaviour, self.behaviour.current_behaviour), + ).behaviour_id + == self.behaviour_class.auto_behaviour_id() + ) + self.behaviour.context.params.__dict__["reset_pause_duration"] = 0.1 + self.behaviour.act_wrapper() + time.sleep(0.3) + self.behaviour.act_wrapper() + self.mock_a2a_transaction() + self._test_done_flag_set() + self.end_round(TransactionSettlementEvent.DONE) + behaviour = cast(BaseBehaviour, self.behaviour.current_behaviour) + assert behaviour.behaviour_id == self.next_behaviour_class.auto_behaviour_id() diff --git a/packages/valory/skills/transaction_settlement_abci/tests/test_dialogues.py b/packages/valory/skills/transaction_settlement_abci/tests/test_dialogues.py new file mode 100644 index 0000000..519ea4d --- /dev/null +++ b/packages/valory/skills/transaction_settlement_abci/tests/test_dialogues.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test the dialogues.py module of the skill.""" + +# pylint: skip-file + +import packages.valory.skills.transaction_settlement_abci.dialogues # noqa + + +def test_import() -> None: + """Test that the 'dialogues.py' Python module can be imported.""" diff --git a/packages/valory/skills/transaction_settlement_abci/tests/test_handlers.py b/packages/valory/skills/transaction_settlement_abci/tests/test_handlers.py new file mode 100644 index 0000000..c458846 --- /dev/null +++ b/packages/valory/skills/transaction_settlement_abci/tests/test_handlers.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2022 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test the dialogues.py module of the skill.""" + +# pylint: skip-file + +import packages.valory.skills.transaction_settlement_abci.handlers # noqa + + +def test_import() -> None: + """Test that the 'handlers.py' Python module can be imported.""" diff --git a/packages/valory/skills/transaction_settlement_abci/tests/test_models.py b/packages/valory/skills/transaction_settlement_abci/tests/test_models.py new file mode 100644 index 0000000..746f0a3 --- /dev/null +++ b/packages/valory/skills/transaction_settlement_abci/tests/test_models.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2022-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +# pylint: disable=unused-import + +"""Test the models.py module of the skill.""" +from typing import Any, Dict +from unittest.mock import MagicMock + +import pytest +import yaml +from aea.exceptions import AEAEnforceError + +from packages.valory.skills.abstract_round_abci.test_tools.base import DummyContext +from packages.valory.skills.transaction_settlement_abci.models import ( + TransactionParams, + _MINIMUM_VALIDATE_TIMEOUT, +) +from packages.valory.skills.transaction_settlement_abci.tests import PACKAGE_DIR + + +class TestTransactionParams: # pylint: disable=too-few-public-methods + """Test TransactionParams class.""" + + default_config: Dict + + def setup_class(self) -> None: + """Read the default config only once.""" + skill_yaml = PACKAGE_DIR / "skill.yaml" + with open(skill_yaml, "r", encoding="utf-8") as skill_file: + skill = yaml.safe_load(skill_file) + self.default_config = skill["models"]["params"]["args"] + + def test_ensure_validate_timeout( # pylint: disable=no-self-use + self, + ) -> None: + """Test that `_ensure_validate_timeout` raises when `validate_timeout` is lower than the allowed minimum.""" + dummy_value = 0 + mock_args, mock_kwargs = ( + MagicMock(), + { + **self.default_config, + "validate_timeout": dummy_value, + "skill_context": DummyContext(), + }, + ) + with pytest.raises( + expected_exception=AEAEnforceError, + match=f"`validate_timeout` must be greater than or equal to {_MINIMUM_VALIDATE_TIMEOUT}", + ): + TransactionParams(mock_args, **mock_kwargs) + + @pytest.mark.parametrize( + "gas_params", + [ + {}, + {"gas_price": 1}, + {"max_fee_per_gas": 1}, + {"max_priority_fee_per_gas": 1}, + { + "gas_price": 1, + "max_fee_per_gas": 1, + "max_priority_fee_per_gas": 1, + }, + ], + ) + def test_gas_params(self, gas_params: Dict[str, Any]) -> None: + """Test that gas params are being handled properly.""" + mock_args, mock_kwargs = ( + MagicMock(), + { + **self.default_config, + "gas_params": gas_params, + "skill_context": DummyContext(), + }, + ) + params = TransactionParams(mock_args, **mock_kwargs) + # verify that the gas params are being set properly + for key, value in gas_params.items(): + assert getattr(params.gas_params, key) == value diff --git a/packages/valory/skills/transaction_settlement_abci/tests/test_payload_tools.py b/packages/valory/skills/transaction_settlement_abci/tests/test_payload_tools.py new file mode 100644 index 0000000..91a90cf --- /dev/null +++ b/packages/valory/skills/transaction_settlement_abci/tests/test_payload_tools.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Tests for valory/transaction settlement skill's payload tools.""" + +# pylint: skip-file + +import pytest + +from packages.valory.contracts.gnosis_safe.contract import SafeOperation +from packages.valory.skills.transaction_settlement_abci.payload_tools import ( + NULL_ADDRESS, + PayloadDeserializationError, + VerificationStatus, + hash_payload_to_hex, + skill_input_hex_to_payload, + tx_hist_hex_to_payload, + tx_hist_payload_to_hex, +) + + +class TestTxHistPayloadEncodingDecoding: + """Tests for the transaction history's payload encoding - decoding.""" + + @staticmethod + @pytest.mark.parametrize( + "verification_status, tx_hash", + ( + ( + VerificationStatus.VERIFIED, + "0xb0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", + ), + (VerificationStatus.ERROR, None), + ), + ) + def test_tx_hist_payload_to_hex_and_back( + verification_status: VerificationStatus, tx_hash: str + ) -> None: + """Test `tx_hist_payload_to_hex` and `tx_hist_hex_to_payload` functions.""" + intermediate = tx_hist_payload_to_hex(verification_status, tx_hash) + verification_status_, tx_hash_ = tx_hist_hex_to_payload(intermediate) + assert verification_status == verification_status_ + assert tx_hash == tx_hash_ + + @staticmethod + def test_invalid_tx_hash_during_serialization() -> None: + """Test encoding when transaction hash is invalid.""" + with pytest.raises(ValueError): + tx_hist_payload_to_hex(VerificationStatus.VERIFIED, "") + + @staticmethod + @pytest.mark.parametrize( + "payload", + ("0000000000000000000000000000000000000000000000000000000000000008", ""), + ) + def test_invalid_payloads_during_deserialization(payload: str) -> None: + """Test decoding payload is invalid.""" + with pytest.raises(PayloadDeserializationError): + tx_hist_hex_to_payload(payload) + + +@pytest.mark.parametrize("use_flashbots", (True, False)) +def test_payload_to_hex_and_back(use_flashbots: bool) -> None: + """Test `payload_to_hex` function.""" + tx_params = dict( + safe_tx_hash="b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", + ether_value=0, + safe_tx_gas=40000000, + to_address="0x77E9b2EF921253A171Fa0CB9ba80558648Ff7215", + data=( + b"b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9" + b"b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9" + ), + operation=SafeOperation.CALL.value, + base_gas=0, + safe_gas_price=0, + gas_token=NULL_ADDRESS, + use_flashbots=use_flashbots, + gas_limit=0, + refund_receiver=NULL_ADDRESS, + raise_on_failed_simulation=False, + ) + + intermediate = hash_payload_to_hex(**tx_params) # type: ignore + assert tx_params == skill_input_hex_to_payload(intermediate) diff --git a/packages/valory/skills/transaction_settlement_abci/tests/test_payloads.py b/packages/valory/skills/transaction_settlement_abci/tests/test_payloads.py new file mode 100644 index 0000000..e53cd26 --- /dev/null +++ b/packages/valory/skills/transaction_settlement_abci/tests/test_payloads.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test the payloads of the skill.""" + +# pylint: skip-file + +from typing import Optional + +import pytest + +from packages.valory.skills.transaction_settlement_abci.payloads import ( + CheckTransactionHistoryPayload, + FinalizationTxPayload, + RandomnessPayload, + ResetPayload, + SelectKeeperPayload, + SignaturePayload, + SynchronizeLateMessagesPayload, + ValidatePayload, +) + + +def test_randomness_payload() -> None: + """Test `RandomnessPayload`.""" + + payload = RandomnessPayload(sender="sender", round_id=1, randomness="test") + + assert payload.round_id == 1 + assert payload.randomness == "test" + assert payload.data == {"round_id": 1, "randomness": "test"} + + +def test_select_keeper_payload() -> None: + """Test `SelectKeeperPayload`.""" + + payload = SelectKeeperPayload(sender="sender", keepers="test") + + assert payload.keepers == "test" + assert payload.data == {"keepers": "test"} + + +@pytest.mark.parametrize("vote", (None, True, False)) +def test_validate_payload(vote: Optional[bool]) -> None: + """Test `ValidatePayload`.""" + + payload = ValidatePayload(sender="sender", vote=vote) + + assert payload.vote is vote + assert payload.data == {"vote": vote} + + +def test_tx_history_payload() -> None: + """Test `CheckTransactionHistoryPayload`.""" + + payload = CheckTransactionHistoryPayload(sender="sender", verified_res="test") + + assert payload.verified_res == "test" + assert payload.data == {"verified_res": "test"} + + +def test_synchronize_payload() -> None: + """Test `SynchronizeLateMessagesPayload`.""" + + tx_hashes = "test" + payload = SynchronizeLateMessagesPayload(sender="sender", tx_hashes=tx_hashes) + + assert payload.tx_hashes == tx_hashes + assert payload.data == {"tx_hashes": tx_hashes} + + +def test_signature_payload() -> None: + """Test `SignaturePayload`.""" + + payload = SignaturePayload(sender="sender", signature="sign") + + assert payload.signature == "sign" + assert payload.data == {"signature": "sign"} + + +def test_finalization_tx_payload() -> None: + """Test `FinalizationTxPayload`.""" + + payload = FinalizationTxPayload( + sender="sender", + tx_data={ + "tx_digest": "hash", + "nonce": 0, + "max_fee_per_gas": 0, + "max_priority_fee_per_gas": 0, + }, + ) + + assert payload.data == { + "tx_data": { + "tx_digest": "hash", + "nonce": 0, + "max_fee_per_gas": 0, + "max_priority_fee_per_gas": 0, + } + } + + +def test_reset_payload() -> None: + """Test `ResetPayload`.""" + + payload = ResetPayload(sender="sender", period_count=1) + + assert payload.period_count == 1 + assert payload.data == {"period_count": 1} diff --git a/packages/valory/skills/transaction_settlement_abci/tests/test_rounds.py b/packages/valory/skills/transaction_settlement_abci/tests/test_rounds.py new file mode 100644 index 0000000..ecdb1af --- /dev/null +++ b/packages/valory/skills/transaction_settlement_abci/tests/test_rounds.py @@ -0,0 +1,1023 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Tests for valory/registration_abci skill's rounds.""" + +# pylint: skip-file + +import hashlib +import logging # noqa: F401 +from collections import deque +from typing import ( + Any, + Deque, + Dict, + FrozenSet, + List, + Mapping, + Optional, + Type, + Union, + cast, +) +from unittest import mock +from unittest.mock import MagicMock + +import pytest + +from packages.valory.skills.abstract_round_abci.base import ( + ABCIAppInternalError, + AbciAppDB, +) +from packages.valory.skills.abstract_round_abci.base import ( + BaseSynchronizedData as SynchronizedData, +) +from packages.valory.skills.abstract_round_abci.base import ( + BaseTxPayload, + CollectSameUntilThresholdRound, + CollectionRound, + MAX_INT_256, + TransactionNotValidError, + VotingRound, +) +from packages.valory.skills.abstract_round_abci.test_tools.rounds import ( + BaseCollectDifferentUntilThresholdRoundTest, + BaseCollectNonEmptyUntilThresholdRound, + BaseCollectSameUntilThresholdRoundTest, + BaseOnlyKeeperSendsRoundTest, + BaseVotingRoundTest, +) +from packages.valory.skills.transaction_settlement_abci.payload_tools import ( + VerificationStatus, +) +from packages.valory.skills.transaction_settlement_abci.payloads import ( + CheckTransactionHistoryPayload, + FinalizationTxPayload, + RandomnessPayload, + ResetPayload, + SelectKeeperPayload, + SignaturePayload, + SynchronizeLateMessagesPayload, + ValidatePayload, +) +from packages.valory.skills.transaction_settlement_abci.rounds import ( + CheckTransactionHistoryRound, + CollectSignatureRound, +) +from packages.valory.skills.transaction_settlement_abci.rounds import ( + Event as TransactionSettlementEvent, +) +from packages.valory.skills.transaction_settlement_abci.rounds import ( + FinalizationRound, + ResetRound, + SelectKeeperTransactionSubmissionARound, + SelectKeeperTransactionSubmissionBAfterTimeoutRound, + SelectKeeperTransactionSubmissionBRound, + SynchronizeLateMessagesRound, +) +from packages.valory.skills.transaction_settlement_abci.rounds import ( + SynchronizedData as TransactionSettlementSynchronizedSata, +) +from packages.valory.skills.transaction_settlement_abci.rounds import ( + TX_HASH_LENGTH, + ValidateTransactionRound, +) + + +MAX_PARTICIPANTS: int = 4 +RANDOMNESS: str = "d1c29dce46f979f9748210d24bce4eae8be91272f5ca1a6aea2832d3dd676f51" +DUMMY_RANDOMNESS = hashlib.sha256("hash".encode() + str(0).encode()).hexdigest() + + +def get_participants() -> FrozenSet[str]: + """Participants""" + return frozenset([f"agent_{i}" for i in range(MAX_PARTICIPANTS)]) + + +def get_participant_to_randomness( + participants: FrozenSet[str], round_id: int +) -> Dict[str, RandomnessPayload]: + """participant_to_randomness""" + return { + participant: RandomnessPayload( + sender=participant, + round_id=round_id, + randomness=RANDOMNESS, + ) + for participant in participants + } + + +def get_most_voted_randomness() -> str: + """most_voted_randomness""" + return RANDOMNESS + + +def get_participant_to_selection( + participants: FrozenSet[str], + keepers: str, +) -> Dict[str, SelectKeeperPayload]: + """participant_to_selection""" + return { + participant: SelectKeeperPayload(sender=participant, keepers=keepers) + for participant in participants + } + + +def get_participant_to_period_count( + participants: FrozenSet[str], period_count: int +) -> Dict[str, ResetPayload]: + """participant_to_selection""" + return { + participant: ResetPayload(sender=participant, period_count=period_count) + for participant in participants + } + + +def get_safe_contract_address() -> str: + """safe_contract_address""" + return "0x6f6ab56aca12" + + +def get_participant_to_votes( + participants: FrozenSet[str], vote: Optional[bool] = True +) -> Dict[str, ValidatePayload]: + """participant_to_votes""" + return { + participant: ValidatePayload(sender=participant, vote=vote) + for participant in participants + } + + +def get_participant_to_votes_serialized( + participants: FrozenSet[str], vote: Optional[bool] = True +) -> Dict[str, Dict[str, Any]]: + """participant_to_votes""" + return CollectionRound.serialize_collection( + get_participant_to_votes(participants, vote) + ) + + +def get_most_voted_tx_hash() -> str: + """most_voted_tx_hash""" + return "tx_hash" + + +def get_participant_to_signature( + participants: FrozenSet[str], +) -> Dict[str, SignaturePayload]: + """participant_to_signature""" + return { + participant: SignaturePayload(sender=participant, signature="signature") + for participant in participants + } + + +def get_final_tx_hash() -> str: + """final_tx_hash""" + return "tx_hash" + + +def get_participant_to_check( + participants: FrozenSet[str], + status: str, + tx_hash: str, +) -> Dict[str, CheckTransactionHistoryPayload]: + """Get participants to check""" + return { + participant: CheckTransactionHistoryPayload( + sender=participant, + verified_res=status + tx_hash, + ) + for participant in participants + } + + +def get_participant_to_late_arriving_tx_hashes( + participants: FrozenSet[str], +) -> Dict[str, SynchronizeLateMessagesPayload]: + """participant_to_selection""" + return { + participant: SynchronizeLateMessagesPayload( + sender=participant, tx_hashes="1" * TX_HASH_LENGTH + "2" * TX_HASH_LENGTH + ) + for participant in participants + } + + +def get_late_arriving_tx_hashes_deserialized() -> Dict[str, List[str]]: + """Get dummy late-arriving tx hashes.""" + # We want the tx hashes to have a size which can be divided by 64 to be able to parse it. + # Otherwise, they are not valid. + return { + "sender": [ + "t" * TX_HASH_LENGTH, + "e" * TX_HASH_LENGTH, + "s" * TX_HASH_LENGTH, + "t" * TX_HASH_LENGTH, + ] + } + + +def get_late_arriving_tx_hashes_serialized() -> Dict[str, str]: + """Get dummy late-arriving tx hashes.""" + # We want the tx hashes to have a size which can be divided by 64 to be able to parse it. + # Otherwise, they are not valid. + deserialized = get_late_arriving_tx_hashes_deserialized() + return {sender: "".join(hash_) for sender, hash_ in deserialized.items()} + + +def get_keepers(keepers: Deque[str], retries: int = 1) -> str: + """Get dummy keepers.""" + return retries.to_bytes(32, "big").hex() + "".join(keepers) + + +class BaseValidateRoundTest(BaseVotingRoundTest): + """Test BaseValidateRound.""" + + test_class: Type[VotingRound] + test_payload: Type[ValidatePayload] + + def test_positive_votes( + self, + ) -> None: + """Test ValidateRound.""" + + self.synchronized_data.update(tx_hashes_history="t" * 66) + + test_round = self.test_class( + synchronized_data=self.synchronized_data, + context=MagicMock(), + ) + + self._complete_run( + self._test_voting_round_positive( + test_round=test_round, + round_payloads=get_participant_to_votes(self.participants), + synchronized_data_update_fn=lambda _synchronized_data, _: _synchronized_data.update( + participant_to_votes=get_participant_to_votes_serialized( + self.participants + ) + ), + synchronized_data_attr_checks=[ + lambda _synchronized_data: _synchronized_data.participant_to_votes.keys() + ], + exit_event=self._event_class.DONE, + ) + ) + + def test_negative_votes( + self, + ) -> None: + """Test ValidateRound.""" + + test_round = self.test_class( + synchronized_data=self.synchronized_data, + context=MagicMock(), + ) + + self._complete_run( + self._test_voting_round_negative( + test_round=test_round, + round_payloads=get_participant_to_votes(self.participants, vote=False), + synchronized_data_update_fn=lambda _synchronized_data, _: _synchronized_data.update( + participant_to_votes=get_participant_to_votes_serialized( + self.participants, vote=False + ) + ), + synchronized_data_attr_checks=[], + exit_event=self._event_class.NEGATIVE, + ) + ) + + def test_none_votes( + self, + ) -> None: + """Test ValidateRound.""" + + test_round = self.test_class( + synchronized_data=self.synchronized_data, + context=MagicMock(), + ) + + self._complete_run( + self._test_voting_round_none( + test_round=test_round, + round_payloads=get_participant_to_votes(self.participants, vote=None), + synchronized_data_update_fn=lambda _synchronized_data, _: _synchronized_data.update( + participant_to_votes=get_participant_to_votes_serialized( + self.participants, vote=None + ) + ), + synchronized_data_attr_checks=[], + exit_event=self._event_class.NONE, + ) + ) + + +class BaseSelectKeeperRoundTest(BaseCollectSameUntilThresholdRoundTest): + """Test SelectKeeperTransactionSubmissionARound""" + + test_class: Type[CollectSameUntilThresholdRound] + test_payload: Type[BaseTxPayload] + + _synchronized_data_class = SynchronizedData + + @staticmethod + def _participant_to_selection( + participants: FrozenSet[str], keepers: str + ) -> Mapping[str, BaseTxPayload]: + """Get participant to selection""" + return get_participant_to_selection(participants, keepers) + + def test_run( + self, + most_voted_payload: str = "keeper", + keepers: str = "", + exit_event: Optional[Any] = None, + ) -> None: + """Run tests.""" + test_round = self.test_class( + synchronized_data=self.synchronized_data.update( + keepers=keepers, + ), + context=MagicMock(), + ) + + self._complete_run( + self._test_round( + test_round=test_round, + round_payloads=self._participant_to_selection( + self.participants, most_voted_payload + ), + synchronized_data_update_fn=lambda _synchronized_data, _test_round: _synchronized_data.update( + participant_to_selection=CollectionRound.serialize_collection( + self._participant_to_selection( + self.participants, most_voted_payload + ) + ) + ), + synchronized_data_attr_checks=[ + lambda _synchronized_data: _synchronized_data.participant_to_selection.keys() + if exit_event is None + else None + ], + most_voted_payload=most_voted_payload, + exit_event=self._event_class.DONE if exit_event is None else exit_event, + ) + ) + + +class TestSelectKeeperTransactionSubmissionARound(BaseSelectKeeperRoundTest): + """Test SelectKeeperTransactionSubmissionARound""" + + test_class = SelectKeeperTransactionSubmissionARound + test_payload = SelectKeeperPayload + _synchronized_data_class = TransactionSettlementSynchronizedSata + _event_class = TransactionSettlementEvent + + @pytest.mark.parametrize( + "most_voted_payload, keepers, exit_event", + ( + ( + "incorrectly_serialized", + "", + TransactionSettlementEvent.INCORRECT_SERIALIZATION, + ), + ( + int(1).to_bytes(32, "big").hex() + "new_keeper" + "-" * 32, + "", + TransactionSettlementEvent.DONE, + ), + ), + ) + def test_run( + self, + most_voted_payload: str, + keepers: str, + exit_event: TransactionSettlementEvent, + ) -> None: + """Run tests.""" + super().test_run(most_voted_payload, keepers, exit_event) + + +class TestSelectKeeperTransactionSubmissionBRound( + TestSelectKeeperTransactionSubmissionARound +): + """Test SelectKeeperTransactionSubmissionBRound.""" + + test_class = SelectKeeperTransactionSubmissionBRound + + @pytest.mark.parametrize( + "most_voted_payload, keepers, exit_event", + ( + ( + int(1).to_bytes(32, "big").hex() + "new_keeper" + "-" * 32, + "", + TransactionSettlementEvent.DONE, + ), + ( + int(1).to_bytes(32, "big").hex() + "new_keeper" + "-" * 32, + int(1).to_bytes(32, "big").hex() + + "".join( + [keeper + "-" * 30 for keeper in ("test_keeper1", "test_keeper2")] + ), + TransactionSettlementEvent.DONE, + ), + ), + ) + def test_run( + self, + most_voted_payload: str, + keepers: str, + exit_event: TransactionSettlementEvent, + ) -> None: + """Run tests.""" + super().test_run(most_voted_payload, keepers, exit_event) + + +class TestSelectKeeperTransactionSubmissionBAfterTimeoutRound( + TestSelectKeeperTransactionSubmissionBRound +): + """Test SelectKeeperTransactionSubmissionBAfterTimeoutRound.""" + + test_class = SelectKeeperTransactionSubmissionBAfterTimeoutRound + + @mock.patch.object( + TransactionSettlementSynchronizedSata, + "keepers_threshold_exceeded", + new_callable=mock.PropertyMock, + ) + @pytest.mark.parametrize( + "keepers", (f"{int(1).to_bytes(32, 'big').hex()}keeper" + "-" * 36,) + ) + @pytest.mark.parametrize( + "attrs, threshold_exceeded, exit_event", + ( + ( + { + "tx_hashes_history": "t" * 66, + "missed_messages": {f"keeper{'-' * 36}": 10}, + }, + True, + # Since the threshold has been exceeded, we should return a `CHECK_HISTORY` event. + TransactionSettlementEvent.CHECK_HISTORY, + ), + ( + { + "missed_messages": {f"keeper{'-' * 36}": 10}, + }, + True, + TransactionSettlementEvent.CHECK_LATE_ARRIVING_MESSAGE, + ), + ( + { + "missed_messages": {f"keeper{'-' * 36}": 10}, + }, + False, + TransactionSettlementEvent.DONE, + ), + ), + ) + def test_run( + self, + threshold_exceeded_mock: mock.PropertyMock, + keepers: str, + attrs: Dict[str, Union[str, int]], + threshold_exceeded: bool, + exit_event: TransactionSettlementEvent, + ) -> None: + """Test `SelectKeeperTransactionSubmissionBAfterTimeoutRound`.""" + self.synchronized_data.update(participant_to_selection=dict.fromkeys(self.participants), **attrs) # type: ignore + threshold_exceeded_mock.return_value = threshold_exceeded + most_voted_payload = int(1).to_bytes(32, "big").hex() + "new_keeper" + "-" * 32 + super().test_run(most_voted_payload, keepers, exit_event) + initial_missed_messages = cast(Dict[str, int], (attrs["missed_messages"])) + expected_missed_messages = { + sender: missed + 1 for sender, missed in initial_missed_messages.items() + } + synchronized_data = cast( + TransactionSettlementSynchronizedSata, self.synchronized_data + ) + assert synchronized_data.missed_messages == expected_missed_messages + + +class TestFinalizationRound(BaseOnlyKeeperSendsRoundTest): + """Test FinalizationRound.""" + + _synchronized_data_class = TransactionSettlementSynchronizedSata + _event_class = TransactionSettlementEvent + _round_class = FinalizationRound + + @pytest.mark.parametrize( + "tx_hashes_history, tx_digest, missed_messages, status, exit_event", + ( + ( + "", + "", + {"test": 1}, + VerificationStatus.ERROR.value, + TransactionSettlementEvent.CHECK_LATE_ARRIVING_MESSAGE, + ), + ( + "", + "", + {}, + VerificationStatus.ERROR.value, + TransactionSettlementEvent.FINALIZATION_FAILED, + ), + ( + "t" * 66, + "", + {}, + VerificationStatus.VERIFIED.value, + TransactionSettlementEvent.CHECK_HISTORY, + ), + ( + "t" * 66, + "", + {}, + VerificationStatus.ERROR.value, + TransactionSettlementEvent.CHECK_HISTORY, + ), + ( + "", + "", + {}, + VerificationStatus.PENDING.value, + TransactionSettlementEvent.FINALIZATION_FAILED, + ), + ( + "", + "tx_digest" + "t" * 57, + {}, + VerificationStatus.PENDING.value, + TransactionSettlementEvent.DONE, + ), + ( + "t" * 66, + "tx_digest" + "t" * 57, + {}, + VerificationStatus.PENDING.value, + TransactionSettlementEvent.DONE, + ), + ( + "t" * 66, + "", + {}, + VerificationStatus.INSUFFICIENT_FUNDS.value, + TransactionSettlementEvent.INSUFFICIENT_FUNDS, + ), + ), + ) + def test_finalization_round( + self, + tx_hashes_history: str, + tx_digest: str, + missed_messages: int, + status: int, + exit_event: TransactionSettlementEvent, + ) -> None: + """Runs tests.""" + keeper_retries = 2 + blacklisted_keepers = "" + self.participants = frozenset([f"agent_{i}" + "-" * 35 for i in range(4)]) + keepers = deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35)) + self.synchronized_data = cast( + TransactionSettlementSynchronizedSata, + self.synchronized_data.update( + participants=tuple(self.participants), + missed_messages=missed_messages, + tx_hashes_history=tx_hashes_history, + keepers=get_keepers(keepers, keeper_retries), + blacklisted_keepers=blacklisted_keepers, + ), + ) + + sender = keepers[0] + tx_hashes_history += ( + tx_digest + if exit_event == TransactionSettlementEvent.DONE + else tx_hashes_history + ) + if status == VerificationStatus.INSUFFICIENT_FUNDS.value: + popped = keepers.popleft() + blacklisted_keepers += popped + keeper_retries = 1 + + test_round = self._round_class( + synchronized_data=self.synchronized_data, + context=MagicMock(), + ) + + self._complete_run( + self._test_round( + test_round=test_round, + keeper_payloads=FinalizationTxPayload( + sender=sender, + tx_data={ + "status_value": status, + "serialized_keepers": get_keepers(keepers, keeper_retries), + "blacklisted_keepers": blacklisted_keepers, + "tx_hashes_history": tx_hashes_history, + "received_hash": bool(tx_digest), + }, + ), + synchronized_data_update_fn=lambda _synchronized_data, _: _synchronized_data.update( + tx_hashes_history=tx_hashes_history, + blacklisted_keepers=blacklisted_keepers, + keepers=get_keepers(keepers, keeper_retries), + keeper_retries=keeper_retries, + final_verification_status=VerificationStatus(status).value, + ), + synchronized_data_attr_checks=[ + lambda _synchronized_data: _synchronized_data.tx_hashes_history, + lambda _synchronized_data: _synchronized_data.blacklisted_keepers, + lambda _synchronized_data: _synchronized_data.keepers, + lambda _synchronized_data: _synchronized_data.keeper_retries, + lambda _synchronized_data: _synchronized_data.final_verification_status, + ], + exit_event=exit_event, + ) + ) + + def test_finalization_round_no_tx_data(self) -> None: + """Test finalization round when `tx_data` is `None`.""" + keepers = deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35)) + keeper_retries = 2 + self.synchronized_data = cast( + TransactionSettlementSynchronizedSata, + self.synchronized_data.update( + participants=tuple(f"agent_{i}" + "-" * 35 for i in range(4)), + keepers=get_keepers(keepers, keeper_retries), + ), + ) + + sender = keepers[0] + + test_round = self._round_class( + synchronized_data=self.synchronized_data, + context=MagicMock(), + ) + + self._complete_run( + self._test_round( + test_round=test_round, + keeper_payloads=FinalizationTxPayload( + sender=sender, + tx_data=None, + ), + synchronized_data_update_fn=lambda _synchronized_data, _: _synchronized_data, + synchronized_data_attr_checks=[ + lambda _synchronized_data: _synchronized_data.tx_hashes_history, + lambda _synchronized_data: _synchronized_data.blacklisted_keepers, + lambda _synchronized_data: _synchronized_data.keepers, + lambda _synchronized_data: _synchronized_data.keeper_retries, + lambda _synchronized_data: _synchronized_data.final_verification_status, + ], + exit_event=TransactionSettlementEvent.FINALIZATION_FAILED, + ) + ) + + +class TestCollectSignatureRound(BaseCollectDifferentUntilThresholdRoundTest): + """Test CollectSignatureRound.""" + + _synchronized_data_class = TransactionSettlementSynchronizedSata + _event_class = TransactionSettlementEvent + + def test_run( + self, + ) -> None: + """Runs tests.""" + + test_round = CollectSignatureRound( + synchronized_data=self.synchronized_data, + context=MagicMock(), + ) + + self._complete_run( + self._test_round( + test_round=test_round, + round_payloads=get_participant_to_signature(self.participants), + synchronized_data_update_fn=lambda _synchronized_data, _: _synchronized_data, + synchronized_data_attr_checks=[], + exit_event=self._event_class.DONE, + ) + ) + + +class TestValidateTransactionRound(BaseValidateRoundTest): + """Test ValidateRound.""" + + test_class = ValidateTransactionRound + _event_class = TransactionSettlementEvent + _synchronized_data_class = TransactionSettlementSynchronizedSata + + +class TestCheckTransactionHistoryRound(BaseCollectSameUntilThresholdRoundTest): + """Test CheckTransactionHistoryRound""" + + _event_class = TransactionSettlementEvent + _synchronized_data_class = TransactionSettlementSynchronizedSata + + @pytest.mark.parametrize( + "expected_status, expected_tx_hash, missed_messages, expected_event", + ( + ( + "0000000000000000000000000000000000000000000000000000000000000001", + "b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", + {}, + TransactionSettlementEvent.DONE, + ), + ( + "0000000000000000000000000000000000000000000000000000000000000002", + "b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", + {}, + TransactionSettlementEvent.NEGATIVE, + ), + ( + "0000000000000000000000000000000000000000000000000000000000000003", + "b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", + {}, + TransactionSettlementEvent.NONE, + ), + ( + "0000000000000000000000000000000000000000000000000000000000000007", + "b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", + {}, + TransactionSettlementEvent.NONE, + ), + ( + "0000000000000000000000000000000000000000000000000000000000000002", + "b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", + {"test": 1}, + TransactionSettlementEvent.CHECK_LATE_ARRIVING_MESSAGE, + ), + ), + ) + def test_run( + self, + expected_status: str, + expected_tx_hash: str, + missed_messages: int, + expected_event: TransactionSettlementEvent, + ) -> None: + """Run tests.""" + keepers = get_keepers(deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35))) + self.synchronized_data.update(missed_messages=missed_messages, keepers=keepers) + + test_round = CheckTransactionHistoryRound( + synchronized_data=self.synchronized_data, + context=MagicMock(), + ) + + self._complete_run( + self._test_round( + test_round=test_round, + round_payloads=get_participant_to_check( + self.participants, expected_status, expected_tx_hash + ), + synchronized_data_update_fn=lambda synchronized_data, _: synchronized_data.update( + participant_to_check=CollectionRound.serialize_collection( + get_participant_to_check( + self.participants, expected_status, expected_tx_hash + ) + ), + final_verification_status=int(expected_status), + tx_hashes_history=[expected_tx_hash], + keepers=keepers, + final_tx_hash="0xb0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", + ), + synchronized_data_attr_checks=[ + lambda _synchronized_data: _synchronized_data.final_verification_status, + lambda _synchronized_data: _synchronized_data.final_tx_hash, + lambda _synchronized_data: _synchronized_data.keepers, + ] + if expected_event + not in { + TransactionSettlementEvent.NEGATIVE, + TransactionSettlementEvent.CHECK_LATE_ARRIVING_MESSAGE, + } + else [ + lambda _synchronized_data: _synchronized_data.final_verification_status, + lambda _synchronized_data: _synchronized_data.keepers, + ], + most_voted_payload=expected_status + expected_tx_hash, + exit_event=expected_event, + ) + ) + + +class TestSynchronizeLateMessagesRound(BaseCollectNonEmptyUntilThresholdRound): + """Test `SynchronizeLateMessagesRound`.""" + + _event_class = TransactionSettlementEvent + _synchronized_data_class = TransactionSettlementSynchronizedSata + + @pytest.mark.parametrize( + "missed_messages, expected_event", + ( + ( + {f"agent_{i}": 0 for i in range(4)}, + TransactionSettlementEvent.SUSPICIOUS_ACTIVITY, + ), + ({f"agent_{i}": 2 for i in range(4)}, TransactionSettlementEvent.DONE), + ), + ) + def test_runs( + self, missed_messages: int, expected_event: TransactionSettlementEvent + ) -> None: + """Runs tests.""" + self.synchronized_data.update(missed_messages=missed_messages) + test_round = SynchronizeLateMessagesRound( + synchronized_data=self.synchronized_data, + context=MagicMock(), + ) + late_arriving_tx_hashes = { + p: "".join(("1" * TX_HASH_LENGTH, "2" * TX_HASH_LENGTH)) + for p in self.participants + } + test_round.required_block_confirmations = 0 + self._complete_run( + self._test_round( + test_round=test_round, + round_payloads=get_participant_to_late_arriving_tx_hashes( + self.participants + ), + synchronized_data_update_fn=lambda _synchronized_data, _: _synchronized_data.update( + late_arriving_tx_hashes=late_arriving_tx_hashes, + suspects=tuple() + if expected_event == TransactionSettlementEvent.DONE + else tuple(sorted(late_arriving_tx_hashes.keys())), + ), + synchronized_data_attr_checks=[ + lambda _synchronized_data: _synchronized_data.late_arriving_tx_hashes, + lambda _synchronized_data: _synchronized_data.suspects, + ], + exit_event=expected_event, + ) + ) + + @pytest.mark.parametrize("correct_serialization", (True, False)) + def test_check_payload(self, correct_serialization: bool) -> None: + """Test the `check_payload` method.""" + + test_round = SynchronizeLateMessagesRound( + synchronized_data=self.synchronized_data, + context=MagicMock(), + ) + sender = list(test_round.accepting_payloads_from).pop() + hash_length = TX_HASH_LENGTH + if not correct_serialization: + hash_length -= 1 + tx_hashes = "0" * hash_length + payload = SynchronizeLateMessagesPayload(sender=sender, tx_hashes=tx_hashes) + + if correct_serialization: + test_round.check_payload(payload) + return + + with pytest.raises( + TransactionNotValidError, match="Expecting serialized data of chunk size" + ): + test_round.check_payload(payload) + + with pytest.raises( + ABCIAppInternalError, match="Expecting serialized data of chunk size" + ): + test_round.process_payload(payload) + assert payload not in test_round.collection + + +def test_synchronized_datas() -> None: + """Test SynchronizedData.""" + + participants = get_participants() + participant_to_randomness = get_participant_to_randomness(participants, 1) + participant_to_randomness_serialized = CollectionRound.serialize_collection( + participant_to_randomness + ) + most_voted_randomness = get_most_voted_randomness() + participant_to_selection = get_participant_to_selection(participants, "test") + participant_to_selection_serialized = CollectionRound.serialize_collection( + participant_to_selection + ) + safe_contract_address = get_safe_contract_address() + most_voted_tx_hash = get_most_voted_tx_hash() + participant_to_signature = get_participant_to_signature(participants) + participant_to_signature_serialized = CollectionRound.serialize_collection( + participant_to_signature + ) + final_tx_hash = get_final_tx_hash() + actual_keeper_randomness = int(most_voted_randomness, base=16) / MAX_INT_256 + late_arriving_tx_hashes_serialized = get_late_arriving_tx_hashes_serialized() + late_arriving_tx_hashes_deserialized = get_late_arriving_tx_hashes_deserialized() + keepers = get_keepers(deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35))) + expected_keepers = deque(["agent_1" + "-" * 35, "agent_3" + "-" * 35]) + + # test `keeper_retries` property when no `keepers` are set. + synchronized_data_____ = TransactionSettlementSynchronizedSata( + AbciAppDB(setup_data=dict()) + ) + assert synchronized_data_____.keepers == deque() + assert synchronized_data_____.keeper_retries == 0 + + synchronized_data_____ = TransactionSettlementSynchronizedSata( + AbciAppDB( + setup_data=AbciAppDB.data_to_lists( + dict( + all_participants=tuple(participants), + participants=tuple(participants), + consensus_threshold=3, + participant_to_randomness=participant_to_randomness_serialized, + most_voted_randomness=most_voted_randomness, + participant_to_selection=participant_to_selection_serialized, + safe_contract_address=safe_contract_address, + most_voted_tx_hash=most_voted_tx_hash, + participant_to_signature=participant_to_signature_serialized, + final_tx_hash=final_tx_hash, + late_arriving_tx_hashes=late_arriving_tx_hashes_serialized, + keepers=keepers, + blacklisted_keepers="t" * 42, + ) + ), + ) + ) + assert ( + abs(synchronized_data_____.keeper_randomness - actual_keeper_randomness) < 1e-10 + ) # avoid equality comparisons between floats + assert synchronized_data_____.most_voted_randomness == most_voted_randomness + assert synchronized_data_____.safe_contract_address == safe_contract_address + assert synchronized_data_____.most_voted_tx_hash == most_voted_tx_hash + assert synchronized_data_____.participant_to_randomness == participant_to_randomness + assert synchronized_data_____.participant_to_selection == participant_to_selection + assert synchronized_data_____.participant_to_signature == participant_to_signature + assert synchronized_data_____.final_tx_hash == final_tx_hash + assert ( + synchronized_data_____.late_arriving_tx_hashes + == late_arriving_tx_hashes_deserialized + ) + assert synchronized_data_____.keepers == expected_keepers + assert synchronized_data_____.keeper_retries == 1 + assert ( + synchronized_data_____.most_voted_keeper_address == expected_keepers.popleft() + ) + assert synchronized_data_____.keepers_threshold_exceeded + assert synchronized_data_____.blacklisted_keepers == {"t" * 42} + updated_synchronized_data = synchronized_data_____.create() + assert updated_synchronized_data.blacklisted_keepers == set() + + +class TestResetRound(BaseCollectSameUntilThresholdRoundTest): + """Test ResetRound.""" + + _synchronized_data_class = TransactionSettlementSynchronizedSata + _event_class = TransactionSettlementEvent + + def test_runs( + self, + ) -> None: + """Runs tests.""" + randomness = DUMMY_RANDOMNESS + synchronized_data = self.synchronized_data.update( + most_voted_randomness=randomness, + late_arriving_tx_hashes={}, + keepers="", + ) + synchronized_data._db._cross_period_persisted_keys = frozenset( + {"most_voted_randomness"} + ) + test_round = ResetRound( + synchronized_data=synchronized_data, + context=MagicMock(), + ) + next_period_count = 1 + self._complete_run( + self._test_round( + test_round=test_round, + round_payloads=get_participant_to_period_count( + self.participants, next_period_count + ), + synchronized_data_update_fn=lambda _synchronized_data, _: _synchronized_data.create(), + synchronized_data_attr_checks=[], # [lambda _synchronized_data: _synchronized_data.participants], + most_voted_payload=next_period_count, + exit_event=self._event_class.DONE, + ) + ) diff --git a/packages/valory/skills/transaction_settlement_abci/tests/test_tools/__init__.py b/packages/valory/skills/transaction_settlement_abci/tests/test_tools/__init__.py new file mode 100644 index 0000000..d1ae9fb --- /dev/null +++ b/packages/valory/skills/transaction_settlement_abci/tests/test_tools/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2022-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Package for `test_tools` testing.""" diff --git a/packages/valory/skills/transaction_settlement_abci/tests/test_tools/test_integration.py b/packages/valory/skills/transaction_settlement_abci/tests/test_tools/test_integration.py new file mode 100644 index 0000000..84a6792 --- /dev/null +++ b/packages/valory/skills/transaction_settlement_abci/tests/test_tools/test_integration.py @@ -0,0 +1,214 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2022-2023 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test transaction settlement integration test tool.""" + +from pathlib import Path +from typing import cast +from unittest import mock + +import pytest +from aea.exceptions import AEAActException +from web3.types import Nonce, Wei + +from packages.valory.protocols.contract_api import ContractApiMessage +from packages.valory.protocols.ledger_api import LedgerApiMessage +from packages.valory.skills import transaction_settlement_abci +from packages.valory.skills.abstract_round_abci.base import AbciAppDB +from packages.valory.skills.abstract_round_abci.tests.test_tools.base import ( + FSMBehaviourTestToolSetup, +) +from packages.valory.skills.transaction_settlement_abci.behaviours import ( + FinalizeBehaviour, +) +from packages.valory.skills.transaction_settlement_abci.rounds import ( + SynchronizedData as TxSettlementSynchronizedSata, +) +from packages.valory.skills.transaction_settlement_abci.test_tools.integration import ( + _GnosisHelperIntegration, + _SafeConfiguredHelperIntegration, + _TxHelperIntegration, +) + + +DUMMY_TX_HASH = "a" * 234 + + +class Test_SafeConfiguredHelperIntegration(FSMBehaviourTestToolSetup): + """Test_SafeConfiguredHelperIntegration""" + + test_cls = _SafeConfiguredHelperIntegration + + def test_instantiation(self) -> None: + """Test instantiation""" + + self.set_path_to_skill() + self.test_cls.make_ledger_api_connection_callable = ( + lambda *_, **__: mock.MagicMock() + ) + test_instance = cast(_SafeConfiguredHelperIntegration, self.setup_test_cls()) + + assert test_instance.keeper_address in test_instance.safe_owners + + +class Test_GnosisHelperIntegration(FSMBehaviourTestToolSetup): + """Test_SafeConfiguredHelperIntegration""" + + test_cls = _GnosisHelperIntegration + + def test_instantiation(self) -> None: + """Test instantiation""" + + self.set_path_to_skill() + self.test_cls.make_ledger_api_connection_callable = ( + lambda *_, **__: mock.MagicMock() + ) + test_instance = cast(_GnosisHelperIntegration, self.setup_test_cls()) + + assert test_instance.safe_contract_address + assert test_instance.gnosis_instance + assert test_instance.ethereum_api + + +class Test_TxHelperIntegration(FSMBehaviourTestToolSetup): + """Test_SafeConfiguredHelperIntegration""" + + test_cls = _TxHelperIntegration + + def instantiate_test(self) -> _TxHelperIntegration: + """Instantiate the test""" + + path_to_skill = Path(transaction_settlement_abci.__file__).parent + self.set_path_to_skill(path_to_skill=path_to_skill) + self.test_cls.make_ledger_api_connection_callable = ( + lambda *_, **__: mock.MagicMock() + ) + + db = AbciAppDB( + setup_data={"all_participants": [f"agent_{i}" for i in range(4)]} + ) + self.test_cls.tx_settlement_synchronized_data = TxSettlementSynchronizedSata(db) + + test_instance = cast(_TxHelperIntegration, self.setup_test_cls()) + return test_instance + + def test_sign_tx(self) -> None: + """Test sign_tx""" + + test_instance = self.instantiate_test() + test_instance.tx_settlement_synchronized_data.db.update( + most_voted_tx_hash=DUMMY_TX_HASH + ) + + target = test_instance.gnosis_instance.functions.getOwners + return_value = test_instance.safe_owners + with mock.patch.object( + target, "call", new_callable=lambda: lambda: return_value + ): + test_instance.sign_tx() + + def test_sign_tx_failure(self) -> None: + """Test sign_tx failure""" + + test_instance = self.instantiate_test() + test_instance.tx_settlement_synchronized_data.db.update( + most_voted_tx_hash=DUMMY_TX_HASH + ) + + target = test_instance.gnosis_instance.functions.getOwners + with mock.patch.object(target, "call", new_callable=lambda: lambda: {}): + with pytest.raises(AssertionError): + test_instance.sign_tx() + + def test_send_tx(self) -> None: + """Test send tx""" + + test_instance = self.instantiate_test() + + nonce = Nonce(0) + gas_price = {"maxPriorityFeePerGas": Wei(0), "maxFeePerGas": Wei(0)} + behaviour = cast(FinalizeBehaviour, test_instance.behaviour.current_behaviour) + behaviour.params.mutable_params.gas_price = gas_price + behaviour.params.mutable_params.nonce = nonce + + contract_api_message = ContractApiMessage( + performative=ContractApiMessage.Performative.RAW_TRANSACTION, # type: ignore + raw_transaction=ContractApiMessage.RawTransaction( + ledger_id="", body={"nonce": str(nonce), **gas_price} + ), + ) + + ledger_api_message = LedgerApiMessage( + performative=LedgerApiMessage.Performative.TRANSACTION_DIGEST, # type: ignore + transaction_digest=LedgerApiMessage.TransactionDigest( + ledger_id="", body="" + ), + ) + + return_value = contract_api_message, None, ledger_api_message + + with mock.patch.object( + test_instance, + "process_n_messages", + new_callable=lambda: lambda *x, **__: return_value, + ): + test_instance.send_tx() + + def test_validate_tx(self) -> None: + """Test validate_tx""" + + test_instance = self.instantiate_test() + test_instance.tx_settlement_synchronized_data.db.update( + tx_hashes_history="a" * 64 + ) + + contract_api_message = ContractApiMessage( + performative=ContractApiMessage.Performative.STATE, # type: ignore + state=ContractApiMessage.State( + ledger_id="", + body={"verified": True}, + ), + ) + return_value = None, contract_api_message + + with mock.patch.object( + test_instance, + "process_n_messages", + new_callable=lambda: lambda *x, **__: return_value, + ): + test_instance.validate_tx() + + def test_validate_tx_timeout(self) -> None: + """Test validate_tx timeout""" + + test_instance = self.instantiate_test() + synchronized_data = test_instance.tx_settlement_synchronized_data + assert synchronized_data.n_missed_messages == 0 + test_instance.validate_tx(simulate_timeout=True) + assert synchronized_data.n_missed_messages == 1 + + def test_validate_tx_failure(self) -> None: + """Test validate tx failure""" + + test_instance = self.instantiate_test() + + with pytest.raises( + AEAActException, match="FSM design error: tx hash should exist" + ): + test_instance.validate_tx() From 50e81af67cc767cb993f84591a48cb2fcd647eda Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Fri, 18 Oct 2024 20:36:12 +0530 Subject: [PATCH 03/41] feat: updating packages.json --- packages/packages.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/packages.json b/packages/packages.json index b08cb78..9c6a61a 100644 --- a/packages/packages.json +++ b/packages/packages.json @@ -11,7 +11,20 @@ "skill/valory/liquidity_trader_abci/0.1.0": "bafybeihtca6gtyjibj6wkrcdmx3fb3a3bkpdgsphwevkatagxrbqvh6fd4", "skill/valory/optimus_abci/0.1.0": "bafybeifjpvqz2m7qhztib4xcjpbjkuiutrot22flqclg36amvqvrp5ra3e", "agent/valory/optimus/0.1.0": "bafybeida2scmw3qune3n6ru7tuzquuc3mxs2cfivzcncrtlj4ziadv4sqy", - "service/valory/optimus/0.1.0": "bafybeibiiuhqronhgkxjo7x5xve24lkbqom5rqcjxg7vrl6jwavfyypmhu" + "service/valory/optimus/0.1.0": "bafybeibiiuhqronhgkxjo7x5xve24lkbqom5rqcjxg7vrl6jwavfyypmhu", + "custom/eightballer/rsi_strategy/0.1.0": "bafybeigbofp2nqwcxu3rlkuugpc3w6ils3u7glse7c335rddcqg56ybh34", + "custom/eightballer/sma_strategy/0.1.0": "bafybeibve7aw6oye6dc66nl2w6wxuqgxrfh5rilw64vvtfjbld6ocnbcr4", + "custom/eightballer/vwap_momentum/0.1.0": "bafybeih2uiklkrin777kzhyk5khlnj35wapw2m5zeojrhglfosm76xjhym", + "custom/valory/trend_following_strategy/0.1.0": "bafybeibejqrhpxpcszjjq6sqqknk6ja72zfiyebqmpyw32e5hjd5hanx7e", + "custom/eightballer/always_buy/0.1.0": "bafybeic2fpf5ozhkf5jgzmppmfsprqw5ayfx6spgl3owuws464n7mkhpqi", + "skill/valory/trader_decision_maker_abci/0.1.0": "bafybeih7duxvbjevmeez4bvdpahgvefoik3hnpjq3ik5g4jaqbuw5rybtu", + "skill/valory/strategy_evaluator_abci/0.1.0": "bafybeig2mx3abjgjhiizx2kez3462mcygjwlqj5d6jvnovcf7rwzaql43e", + "skill/valory/market_data_fetcher_abci/0.1.0": "bafybeia3kld7ogbaolbxskys7r5ccolhm53fqi4tdkrwnvilfm7gn5ztcm", + "skill/valory/trader_abci/0.1.0": "bafybeiccni66lhpc6nt4hirtakw3xzhera7kqkzmreuznrpuxpckuh455e", + "skill/valory/ipfs_package_downloader/0.1.0": "bafybeid54srronvfqbvcdjgtuhmr4mbndjkpxtgzguykeg4p3wwj3zboyi", + "skill/valory/portfolio_tracker_abci/0.1.0": "bafybeigzyhm3fzoxhggjdexryzqgskafoi6rec4ois34n3asodxn6j3txm", + "agent/valory/solana_trader/0.1.0": "bafybeifmeou6eckov6nu5ni64dzeogncav6yao5hftr3kaybijpk53tocq", + "service/valory/solana_trader/0.1.0": "bafybeia2pv6gjfwciweahiw5asurpl43drgo5yn2ueqwfnnhvco3crzdly" }, "third_party": { "protocol/open_aea/signing/1.0.0": "bafybeihv62fim3wl2bayavfcg3u5e5cxu3b7brtu4cn5xoxd6lqwachasi", From 7d20b215f2f44e5dbcb7ebfab5df0e3578492f2f Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Fri, 18 Oct 2024 20:40:16 +0530 Subject: [PATCH 04/41] chore: Update aea-config.yaml --- packages/valory/agents/optimus/aea-config.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/valory/agents/optimus/aea-config.yaml b/packages/valory/agents/optimus/aea-config.yaml index bdff5a3..2d84566 100644 --- a/packages/valory/agents/optimus/aea-config.yaml +++ b/packages/valory/agents/optimus/aea-config.yaml @@ -264,4 +264,8 @@ models: reset_tendermint_after: ${int:2} retry_attempts: ${int:400} retry_timeout: ${int:3} - request \ No newline at end of file + request_retry_delay: ${float:1.0} + request_timeout: ${float:10.0} + service_id: ${str:solana_trader} + tendermint_url: ${str:http://localhost:26657} + tendermint_com_url: ${str:http://localhost:8080 \ No newline at end of file From c1df2c40d616f55da657d42be6aaeb9d5569772d Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Tue, 22 Oct 2024 09:35:45 +0530 Subject: [PATCH 05/41] feat: Update agent_transition environment variable in config files --- packages/packages.json | 12 +- .../valory/agents/optimus/aea-config.yaml | 257 +++++------ packages/valory/services/optimus/service.yaml | 408 +----------------- .../liquidity_trader_abci/behaviours.py | 32 ++ .../skills/liquidity_trader_abci/models.py | 3 + .../skills/liquidity_trader_abci/payloads.py | 6 + .../skills/liquidity_trader_abci/rounds.py | 38 +- .../skills/liquidity_trader_abci/skill.yaml | 1 + .../valory/skills/optimus_abci/behaviours.py | 21 + .../valory/skills/optimus_abci/composition.py | 38 +- .../valory/skills/optimus_abci/skill.yaml | 129 +++++- scripts/aea-config-replace.py | 4 + 12 files changed, 388 insertions(+), 561 deletions(-) diff --git a/packages/packages.json b/packages/packages.json index 9c6a61a..db01f2a 100644 --- a/packages/packages.json +++ b/packages/packages.json @@ -18,13 +18,13 @@ "custom/valory/trend_following_strategy/0.1.0": "bafybeibejqrhpxpcszjjq6sqqknk6ja72zfiyebqmpyw32e5hjd5hanx7e", "custom/eightballer/always_buy/0.1.0": "bafybeic2fpf5ozhkf5jgzmppmfsprqw5ayfx6spgl3owuws464n7mkhpqi", "skill/valory/trader_decision_maker_abci/0.1.0": "bafybeih7duxvbjevmeez4bvdpahgvefoik3hnpjq3ik5g4jaqbuw5rybtu", - "skill/valory/strategy_evaluator_abci/0.1.0": "bafybeig2mx3abjgjhiizx2kez3462mcygjwlqj5d6jvnovcf7rwzaql43e", - "skill/valory/market_data_fetcher_abci/0.1.0": "bafybeia3kld7ogbaolbxskys7r5ccolhm53fqi4tdkrwnvilfm7gn5ztcm", - "skill/valory/trader_abci/0.1.0": "bafybeiccni66lhpc6nt4hirtakw3xzhera7kqkzmreuznrpuxpckuh455e", + "skill/valory/strategy_evaluator_abci/0.1.0": "bafybeihc7pxnwmgj2wrf7awdeiu2yjyvudlmdyfkahmpkiwq7dyt5aa44u", + "skill/valory/market_data_fetcher_abci/0.1.0": "bafybeieyaop63uqw3nk2mx7nu3yvqp45eioz7rkfn5n3ocvvt3odrddoke", + "skill/valory/trader_abci/0.1.0": "bafybeig7vluejd62szp236nzhhgaaqbhcps2qgjhz2rmwjw2hijck2bfvm", "skill/valory/ipfs_package_downloader/0.1.0": "bafybeid54srronvfqbvcdjgtuhmr4mbndjkpxtgzguykeg4p3wwj3zboyi", - "skill/valory/portfolio_tracker_abci/0.1.0": "bafybeigzyhm3fzoxhggjdexryzqgskafoi6rec4ois34n3asodxn6j3txm", - "agent/valory/solana_trader/0.1.0": "bafybeifmeou6eckov6nu5ni64dzeogncav6yao5hftr3kaybijpk53tocq", - "service/valory/solana_trader/0.1.0": "bafybeia2pv6gjfwciweahiw5asurpl43drgo5yn2ueqwfnnhvco3crzdly" + "skill/valory/portfolio_tracker_abci/0.1.0": "bafybeictj7o35cttmhy43xi25fxyqmfhb7g2rd4yefj6hq2362xukifrpi", + "agent/valory/solana_trader/0.1.0": "bafybeiei6g2i7bntofmpduy75tuulcleewmdiww5poxszj7yohv2wd63cq", + "service/valory/solana_trader/0.1.0": "bafybeib5tasy5wc3cjbb6k42gz4gx3ub43cd67f66iay2fkxxcuxmnuqpy" }, "third_party": { "protocol/open_aea/signing/1.0.0": "bafybeihv62fim3wl2bayavfcg3u5e5cxu3b7brtu4cn5xoxd6lqwachasi", diff --git a/packages/valory/agents/optimus/aea-config.yaml b/packages/valory/agents/optimus/aea-config.yaml index 2d84566..57c6b84 100644 --- a/packages/valory/agents/optimus/aea-config.yaml +++ b/packages/valory/agents/optimus/aea-config.yaml @@ -1,15 +1,13 @@ -agent_name: solana_trader +agent_name: optimus author: valory version: 0.1.0 license: Apache-2.0 -description: Solana trader agent. -aea_version: '>=1.0.0, <2.0.0' +description: An optimism liquidity trader agent. +aea_version: '>=1.19.0, <2.0.0' fingerprint: __init__.py: bafybeigx5mdvnamsqfum5ut7htok2y5vsnu7lrvms5gfvqi7hmv7sfbo3a - README.md: bafybeibm2adzlongvgzyepiiymb3hxpsjb43qgr7j4uydebjzcpdrwm3om fingerprint_ignore_patterns: [] connections: -- eightballer/dcxt:0.1.0:bafybeihes2id6qmqqybvdqfpchu6h3i5mpnhq6lztfxp52twd22iwqxacq - valory/abci:0.1.0:bafybeiejymu4ul62zx6weoibnlsrfprfpjnplhjefz6sr6izgdr4sajlnu - valory/http_client:0.23.0:bafybeihi772xgzpqeipp3fhmvpct4y6e6tpjp4sogwqrnf3wqspgeilg4u - valory/http_server:0.22.0:bafybeihpgu56ovmq4npazdbh6y6ru5i7zuv6wvdglpxavsckyih56smu7m @@ -17,11 +15,10 @@ connections: - valory/ledger:0.19.0:bafybeigntoericenpzvwejqfuc3kqzo2pscs76qoygg5dbj6f4zxusru5e - valory/p2p_libp2p_client:0.1.0:bafybeid3xg5k2ol5adflqloy75ibgljmol6xsvzvezebsg7oudxeeolz7e contracts: -- eightballer/erc_20:0.1.0:bafybeiezbnm3f5zhuj5bsc542isnlh2fki5q4nmm2vsajzps4uuoamofo4 +- valory/gnosis_safe:0.1.0:bafybeib375xmvcplw7ageic2np3hq4yqeijrvd5kl7rrdnyvswats6ngmm +- valory/gnosis_safe_proxy_factory:0.1.0:bafybeicpcpyurm7gxir2gnlsgzeirzomkhcbnzr5txk67zdf4mmg737rtu - valory/multisend:0.1.0:bafybeig5byt5urg2d2bsecufxe5ql7f4mezg3mekfleeh32nmuusx66p4y - valory/service_registry:0.1.0:bafybeihafe524ilngwzavkhwz4er56p7nyar26lfm7lrksfiqvvzo3kdcq -- valory/gnosis_safe:0.1.0:bafybeiho6sbfts3zk3mftrngw37d5qnlvkqtnttt3fzexmcwkeevhu4wwi -- valory/gnosis_safe_proxy_factory:0.1.0:bafybeicpcpyurm7gxir2gnlsgzeirzomkhcbnzr5txk67zdf4mmg737rtu - valory/balancer_weighted_pool:0.1.0:bafybeidyjlrlq3jrbackewedwt5irokhjupxgpqfgur2ri426cap2oqt7a - valory/balancer_vault:0.1.0:bafybeie6twptrkqddget7pjijzob2c4jqmrrtpkwombneh35xx56djz4ru - valory/uniswap_v3_non_fungible_position_manager:0.1.0:bafybeigadr3nyx6tkrual7oqn2qiup35addfevromxjzzlvkiukpyhtz6y @@ -44,16 +41,9 @@ skills: - valory/reset_pause_abci:0.1.0:bafybeif4lgvbzsmzljesxbphycdv52ka7qnihyjrjpfaseclxadcmm6yiq - valory/termination_abci:0.1.0:bafybeiekkpo5qef5zaeagm3si6v45qxcojvtjqe4a5ceccvk4q7k3xi3bi - valory/transaction_settlement_abci:0.1.0:bafybeielv6eivt2z6nforq43xewl2vmpfwpdu2s2vfogobziljnwsclmlm -- valory/market_data_fetcher_abci:0.1.0:bafybeia3kld7ogbaolbxskys7r5ccolhm53fqi4tdkrwnvilfm7gn5ztcm -- valory/strategy_evaluator_abci:0.1.0:bafybeig2mx3abjgjhiizx2kez3462mcygjwlqj5d6jvnovcf7rwzaql43e -- valory/trader_abci:0.1.0:bafybeiccni66lhpc6nt4hirtakw3xzhera7kqkzmreuznrpuxpckuh455e -- valory/trader_decision_maker_abci:0.1.0:bafybeih7duxvbjevmeez4bvdpahgvefoik3hnpjq3ik5g4jaqbuw5rybtu -- valory/ipfs_package_downloader:0.1.0:bafybeid54srronvfqbvcdjgtuhmr4mbndjkpxtgzguykeg4p3wwj3zboyi -- valory/portfolio_tracker_abci:0.1.0:bafybeigzyhm3fzoxhggjdexryzqgskafoi6rec4ois34n3asodxn6j3txm default_ledger: ethereum required_ledgers: - ethereum -- solana default_routing: {} connection_private_key_paths: {} private_key_paths: {} @@ -79,27 +69,16 @@ logging_config: - logfile - console propagate: true +skill_exception_policy: stop_and_exit dependencies: - open-aea-ledger-cosmos: - version: ==1.55.0 - open-aea-ledger-solana: - version: ==1.55.0 open-aea-ledger-ethereum: - version: ==1.55.0 - open-aea-test-autonomy: - version: ==0.15.2 - pyalgotrade: - version: ==0.20 - open-aea-ledger-ethereum-tool: version: ==1.57.0 -skill_exception_policy: stop_and_exit -connection_exception_policy: just_log default_connection: null --- public_id: valory/abci:0.1.0 type: connection config: - target_skill_id: valory/trader_abci:0.1.0 + target_skill_id: valory/optimus_abci:0.1.0 host: ${str:localhost} port: ${int:26658} use_tendermint: ${bool:false} @@ -109,8 +88,8 @@ type: connection config: ledger_apis: ethereum: - address: ${str:https://base.blockpi.network/v1/rpc/public} - chain_id: ${int:8453} + address: ${str:https://virtual.mainnet.rpc.tenderly.co/85a9fd10-356e-4526-b1f6-7148366bf227} + chain_id: ${int:1} poa_chain: ${bool:false} default_gas_price_strategy: ${str:eip1559} base: @@ -119,7 +98,7 @@ config: poa_chain: ${bool:false} default_gas_price_strategy: ${str:eip1559} optimism: - address: ${str:https://mainnet.optimism.io} + address: ${str:https://virtual.optimism.rpc.tenderly.co/3baf4a62-2fa9-448a-91a6-5f6ab95c76be} chain_id: ${int:10} poa_chain: ${bool:false} default_gas_price_strategy: ${str:eip1559} @@ -134,138 +113,134 @@ cert_requests: - identifier: acn ledger_id: ethereum message_format: '{public_key}' - not_after: '2024-01-01' - not_before: '2023-01-01' + not_after: '2023-01-01' + not_before: '2022-01-01' public_key: ${str:02d3a830c9d6ea1ae91936951430dee11f4662f33118b02190693be835359a9d77} - save_path: .certs/acn_cosmos_9005.txt + save_path: .certs/acn_cosmos_11000.txt --- public_id: valory/http_server:0.22.0:bafybeicblltx7ha3ulthg7bzfccuqqyjmihhrvfeztlgrlcoxhr7kf6nbq type: connection config: host: 0.0.0.0 - target_skill_id: valory/trader_abci:0.1.0 + target_skill_id: valory/optimus_abci:0.1.0 --- -public_id: valory/http_client:0.23.0 -type: connection -config: - host: ${str:127.0.0.1} - port: ${int:8000} - timeout: ${int:1200} ---- -public_id: eightballer/dcxt:0.1.0 -type: connection -config: - target_skill_id: eightballer/chained_dex_app:0.1.0 - exchanges: - - name: ${str:balancer} - key_path: ${str:ethereum_private_key.txt} - ledger_id: ${str:base} - rpc_url: ${str:https://base.blockpi.network/v1/rpc/public} - etherscan_api_key: ${str:YOUR_ETHERSCAN_API_KEY} ---- -public_id: valory/ipfs_package_downloader:0.1.0 -type: skill -models: - params: - args: - cleanup_freq: ${int:50} - timeout_limit: ${int:3} - file_hash_to_id: ${list:[["bafybeic2fpf5ozhkf5jgzmppmfsprqw5ayfx6spgl3owuws464n7mkhpqi",["sma_strategy"]]]} - component_yaml_filename: ${str:component.yaml} - entry_point_key: ${str:entry_point} - callable_keys: ${list:["run_callable","transform_callable","evaluate_callable"]} ---- -public_id: valory/trader_abci:0.1.0 +public_id: valory/optimus_abci:0.1.0 type: skill models: benchmark_tool: args: - log_dir: ${str:/benchmarks} - get_balance: - args: - api_id: ${str:get_balance} - headers: - Content-Type: ${str:application/json} - method: ${str:POST} - parameters: ${dict:{}} - response_key: ${str:result:value} - response_type: ${str:int} - error_key: ${str:error:message} - error_type: ${str:str} - retries: ${int:5} - url: ${str:https://api.mainnet-beta.solana.com} - token_accounts: - args: - api_id: ${str:token_accounts} - headers: - Content-Type: ${str:application/json} - method: ${str:POST} - parameters: ${dict:{}} - response_key: ${str:result:value} - response_type: ${str:list} - error_key: ${str:error:message} - error_type: ${str:str} - retries: ${int:5} - url: ${str:https://api.mainnet-beta.solana.com} + log_dir: ${str:/logs} coingecko: args: token_price_endpoint: ${str:https://api.coingecko.com/api/v3/simple/token_price/{asset_platform_id}?contract_addresses={token_address}&vs_currencies=usd} - coin_price_endpoint: ${str:https://api.coingecko.com/api/v3/coins/{token_id}/market_chart?vs_currency=usd&days=1} + coin_price_endpoint: ${str:https://api.coingecko.com/api/v3/simple/price?ids={coin_id}&vs_currencies=usd} api_key: ${str:null} - prices_field: ${str:prices} - requests_per_minute: ${int:5} + requests_per_minute: ${int:30} credits: ${int:10000} rate_limited_code: ${int:429} - tx_settlement_proxy: - args: - api_id: ${str:tx_settlement_proxy} - headers: - Content-Type: ${str:application/json} - method: ${str:POST} - parameters: - amount: ${int:100000000} - slippageBps: ${int:5} - resendAmount: ${int:200} - timeoutInMs: ${int:120000} - priorityFee: ${int:5000000} - response_key: ${str:null} - response_type: ${str:dict} - retries: ${int:5} - url: ${str:http://localhost:3000/tx} + chain_to_platform_id_mapping: ${str:{"optimism":"optimistic-ethereum","base":"base","ethereum":"ethereum"}} params: args: - setup: - all_participants: ${list:["0x0000000000000000000000000000000000000000"]} - consensus_threshold: ${int:null} - safe_contract_address: ${str:0x0000000000000000000000000000000000000000} - cleanup_history_depth: ${int:1} - cleanup_history_depth_current: ${int:null} - drand_public_key: ${str:868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31} + cleanup_history_depth: 1 + cleanup_history_depth_current: null + drand_public_key: 868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31 genesis_config: - genesis_time: ${str:2022-09-26T00:00:00.000000000Z} - chain_id: ${str:chain-c4daS1} + genesis_time: '2022-09-26T00:00:00.000000000Z' + chain_id: chain-c4daS1 consensus_params: block: - max_bytes: ${str:22020096} - max_gas: ${str:-1} - time_iota_ms: ${str:1000} + max_bytes: '22020096' + max_gas: '-1' + time_iota_ms: '1000' evidence: - max_age_num_blocks: ${str:100000} - max_age_duration: ${str:172800000000000} - max_bytes: ${str:1048576} + max_age_num_blocks: '100000' + max_age_duration: '172800000000000' + max_bytes: '1048576' validator: - pub_key_types: ${list:["ed25519"]} - version: ${dict:{}} - voting_power: ${str:10} - init_fallback_gas: ${int:0} - keeper_allowed_retries: ${int:3} - keeper_timeout: ${float:30.0} - max_attempts: ${int:10} - reset_tendermint_after: ${int:2} - retry_attempts: ${int:400} - retry_timeout: ${int:3} - request_retry_delay: ${float:1.0} - request_timeout: ${float:10.0} - service_id: ${str:solana_trader} + pub_key_types: + - ed25519 + version: {} + voting_power: '10' + keeper_timeout: 30.0 + max_attempts: 10 + max_healthcheck: 120 + multisend_address: ${str:0x0000000000000000000000000000000000000000} + termination_sleep: ${int:900} + init_fallback_gas: 0 + keeper_allowed_retries: 3 + reset_pause_duration: ${int:10} + on_chain_service_id: ${int:1} + reset_tendermint_after: ${int:10} + retry_attempts: 400 + retry_timeout: 3 + request_retry_delay: 1.0 + request_timeout: 10.0 + round_timeout_seconds: 30.0 + service_id: optimus + service_registry_address: ${str:null} + setup: + all_participants: ${list:["0x1aCD50F973177f4D320913a9Cc494A9c66922fdF"]} + consensus_threshold: ${int:null} + safe_contract_address: ${str:0x0000000000000000000000000000000000000000} + share_tm_config_on_startup: ${bool:false} + sleep_time: 1 + tendermint_check_sleep_delay: 3 + tendermint_com_url: ${str:http://localhost:8080} + tendermint_max_retries: 5 tendermint_url: ${str:http://localhost:26657} - tendermint_com_url: ${str:http://localhost:8080 \ No newline at end of file + tendermint_p2p_url: ${str:localhost:26656} + use_termination: ${bool:false} + tx_timeout: 10.0 + validate_timeout: 1205 + finalize_timeout: 60.0 + history_check_timeout: 1205 + use_slashing: ${bool:false} + slash_cooldown_hours: ${int:3} + slash_threshold_amount: ${int:10000000000000000} + light_slash_unit_amount: ${int:5000000000000000} + serious_slash_unit_amount: ${int:8000000000000000} + multisend_batch_size: ${int:50} + ipfs_address: ${str:https://gateway.autonolas.tech/ipfs/} + default_chain_id: ${str:optimism} + termination_from_block: ${int:34088325} + allowed_dexs: ${list:["balancerPool", "UniswapV3"]} + initial_assets: ${str:{"ethereum":{"0x0000000000000000000000000000000000000000":"ETH","0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48":"USDC"}}} + safe_contract_addresses: ${str:{"ethereum":"0x0000000000000000000000000000000000000000","base":"0x07e27E181Df065141ee90a4DD43cE4113bc9853C","optimism":"0x07e27E181Df065141ee90a4DD43cE4113bc9853C"}} + merkl_fetch_campaigns_args: ${str:{"url":"https://api.merkl.xyz/v3/campaigns","creator":"","live":"true"}} + allowed_chains: ${list:["optimism","base"]} + gas_reserve: ${str:{"ethereum":1000,"optimism":1000,"base":1000}} + round_threshold: ${int:0} + apr_threshold: ${int:5} + min_balance_multiplier: ${int:5} + multisend_contract_addresses: ${str:{"ethereum":"0x998739BFdAAdde7C933B942a68053933098f9EDa","optimism":"0xbE5b0013D2712DC4faF07726041C27ecFdBC35AD","base":"0x998739BFdAAdde7C933B942a68053933098f9EDa"}} + lifi_advance_routes_url: ${str:https://li.quest/v1/advanced/routes} + lifi_fetch_step_transaction_url: ${str:https://li.quest/v1/advanced/stepTransaction} + lifi_check_status_url: ${str:https://li.quest/v1/status} + lifi_fetch_tools_url: ${str:https://li.quest/v1/tools} + slippage_for_swap: ${float:0.09} + balancer_vault_contract_addresses: ${str:{"optimism":"0xBA12222222228d8Ba445958a75a0704d566BF2C8","base":"0xBA12222222228d8Ba445958a75a0704d566BF2C8"}} + uniswap_position_manager_contract_addresses: ${str:{"optimism":"0xC36442b4a4522E871399CD717aBDD847Ab11FE88","base":"0x03a520b32C04BF3bEEf7BEb72E919cf822Ed34f1"}} + chain_to_chain_key_mapping: ${str:{"ethereum":"eth","optimism":"opt","base":"bas"}} + waiting_period_for_status_check: ${int:10} + max_num_of_retries: ${int:5} + reward_claiming_time_period: ${int:28800} + merkl_distributor_contract_addresses: ${str:{"optimism":"0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae","base":"0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae"}} + intermediate_tokens: ${str:{"ethereum":{"0x0000000000000000000000000000000000000000":{"symbol":"ETH","liquidity_provider":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"},"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2":{"symbol":"WETH","liquidity_provider":"0xF04a5cC80B1E94C69B48f5ee68a08CD2F09A7c3E"},"0xdAC17F958D2ee523a2206206994597C13D831ec7":{"symbol":"USDT","liquidity_provider":"0xcEe284F754E854890e311e3280b767F80797180d"},"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48":{"symbol":"USDC","liquidity_provider":"0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"},"0x6B175474E89094C44Da98b954EedeAC495271d0F":{"symbol":"DAI","liquidity_provider":"0x517F9dD285e75b599234F7221227339478d0FcC8"},"0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84":{"symbol":"stETH","liquidity_provider":"0x4028DAAC072e492d34a3Afdbef0ba7e35D8b55C4"},"0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0":{"symbol":"wstETH","liquidity_provider":"0x109830a1AAaD605BbF02a9dFA7B0B92EC2FB7dAa"},"0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599":{"symbol":"WBTC","liquidity_provider":"0xCBCdF9626bC03E24f779434178A73a0B4bad62eD"},"0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984":{"symbol":"UNI","liquidity_provider":"0x1d42064Fc4Beb5F8aAF85F4617AE8b3b5B8Bd801"}},"optimism":{"0x0000000000000000000000000000000000000000":{"symbol":"ETH","liquidity_provider":"0x4200000000000000000000000000000000000006"},"0x7F5c764cBc14f9669B88837ca1490cCa17c31607":{"symbol":"USDC.e","liquidity_provider":"0xD1F1baD4c9E6c44DeC1e9bF3B94902205c5Cd6C3"},"0x4200000000000000000000000000000000000006":{"symbol":"WETH","liquidity_provider":"0xBA12222222228d8Ba445958a75a0704d566BF2C8"},"0x94b008aA00579c1307B0EF2c499aD98a8ce58e58":{"symbol":"USDT","liquidity_provider":"0xA73C628eaf6e283E26A7b1f8001CF186aa4c0E8E"},"0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1":{"symbol":"DAI","liquidity_provider":"0x03aF20bDAaFfB4cC0A521796a223f7D85e2aAc31"},"0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb":{"symbol":"wstETH","liquidity_provider":"0x04F6C85A1B00F6D9B75f91FD23835974Cc07E65c"},"0x68f180fcCe6836688e9084f035309E29Bf0A2095":{"symbol":"WBTC","liquidity_provider":"0x078f358208685046a11C85e8ad32895DED33A249"},"0x76FB31fb4af56892A25e32cFC43De717950c9278":{"symbol":"AAVE","liquidity_provider":"0xf329e36C7bF6E5E86ce2150875a84Ce77f477375"},"0x4200000000000000000000000000000000000042":{"symbol":"OP","liquidity_provider":"0x2A82Ae142b2e62Cb7D10b55E323ACB1Cab663a26"}},"base":{"0x0000000000000000000000000000000000000000":{"symbol":"ETH","liquidity_provider":"0xd0b53D9277642d899DF5C87A3966A349A798F224"},"0x4200000000000000000000000000000000000006":{"symbol":"WETH","liquidity_provider":"0xBA12222222228d8Ba445958a75a0704d566BF2C8"},"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913":{"symbol":"USDC","liquidity_provider":"0x0B0A5886664376F59C351ba3f598C8A8B4D0A6f3"},"0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA":{"symbol":"USDbC","liquidity_provider":"0x0B25c51637c43decd6CC1C1e3da4518D54ddb528"},"0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb":{"symbol":"DAI","liquidity_provider":"0x927860797d07b1C46fbBe7f6f73D45C7E1BFBb27"},"0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452":{"symbol":"wstETH","liquidity_provider":"0x99CBC45ea5bb7eF3a5BC08FB1B7E56bB2442Ef0D"},"0xB6fe221Fe9EeF5aBa221c348bA20A1Bf5e73624c":{"symbol":"rETH","liquidity_provider":"0x95Fa1ddc9a78273f795e67AbE8f1Cd2Cd39831fF"},"0x532f27101965dd16442E59d40670FaF5eBB142E4":{"symbol":"BRETT","liquidity_provider":"0xBA3F945812a83471d709BCe9C3CA699A19FB46f7"}}}} + merkl_user_rewards_url: ${str:https://api.merkl.xyz/v3/userRewards} + tenderly_bundle_simulation_url: ${str:https://api.tenderly.co/api/v1/account/{tenderly_account_slug}/project/{tenderly_project_slug}/simulate-bundle} + tenderly_access_key: ${str:access_key} + tenderly_account_slug: ${str:account_slug} + tenderly_project_slug: ${str:project_slug} + agent_transition: ${str:agent_transition} + chain_to_chain_id_mapping: ${str:{"optimism":10,"base":8453,"ethereum":1}} + staking_token_contract_address: ${str:0x88996bbdE7f982D93214881756840cE2c77C4992} + staking_activity_checker_contract_address: ${str:0x7Fd1F4b764fA41d19fe3f63C85d12bf64d2bbf68} + staking_threshold_period: ${int:5} + store_path: ${str:/data/} + assets_info_filename: ${str:assets.json} + pool_info_filename: ${str:current_pool.json} + gas_cost_info_filename: ${str:gas_costs.json} + min_swap_amount_threshold: ${int:10} + max_fee_percentage: ${float:0.02} + max_gas_percentage: ${float:0.25} + balancer_graphql_endpoints: ${str:{"optimism":"https://api.studio.thegraph.com/query/75376/balancer-optimism-v2/version/latest","base":"https://api.studio.thegraph.com/query/24660/balancer-base-v2/version/latest"}} \ No newline at end of file diff --git a/packages/valory/services/optimus/service.yaml b/packages/valory/services/optimus/service.yaml index 355fc42..902f3be 100644 --- a/packages/valory/services/optimus/service.yaml +++ b/packages/valory/services/optimus/service.yaml @@ -1,14 +1,13 @@ -name: solana_trader +name: optimus author: valory version: 0.1.0 -description: A set of agents trading tokens on Solana. +description: An optimism liquidity trader service. aea_version: '>=1.0.0, <2.0.0' license: Apache-2.0 -fingerprint: - README.md: bafybeiasyvanypleay7yspri6igebqccpm3a7uvvrlna5yjkzdsnvcbu4q +fingerprint: {} fingerprint_ignore_patterns: [] -agent: valory/solana_trader:0.1.0:bafybeifmeou6eckov6nu5ni64dzeogncav6yao5hftr3kaybijpk53tocq -number_of_agents: 4 +agent: valory/optimus:0.1.0:bafybeida2scmw3qune3n6ru7tuzquuc3mxs2cfivzcncrtlj4ziadv4sqy +number_of_agents: 1 deployment: {} --- public_id: valory/optimus_abci:0.1.0 @@ -68,6 +67,7 @@ models: tenderly_access_key: ${TENDERLY_ACCESS_KEY:str:access_key} tenderly_account_slug: ${TENDERLY_ACCOUNT_SLUG:str:account_slug} tenderly_project_slug: ${TENDERLY_PROJECT_SLUG:str:project_slug} + agent_transition: ${AGENT_TRANSITION:bool:agent_transition} tendermint_p2p_url: ${TENDERMINT_P2P_URL_0:str:optimism_tm_0:26656} service_endpoint_base: ${SERVICE_ENDPOINT_BASE:str:https://optimism.autonolas.tech/} multisend_batch_size: ${MULTISEND_BATCH_SIZE:int:5} @@ -127,398 +127,4 @@ cert_requests: not_after: '2023-01-01' not_before: '2022-01-01' public_key: ${ACN_NODE_PUBLIC_KEY:str:02d3a830c9d6ea1ae91936951430dee11f4662f33118b02190693be835359a9d77} - save_path: .certs/acn_cosmos_11000.txt ---- -public_id: valory/ipfs_package_downloader:0.1.0 -type: skill -0: - models: - params: - args: - cleanup_freq: ${CLEANUP_FREQ:int:50} - timeout_limit: ${TIMEOUT_LIMIT:int:3} - file_hash_to_id: ${FILE_HASH_TO_ID:list:[["bafybeifpqcxwjjlenpa7n3nnx5ornrg7uz5d7h76ugzhiwk45bx3sx3cta",["sma_strategy"]]]} - component_yaml_filename: ${COMPONENT_YAML_FILENAME:str:component.yaml} - entry_point_key: ${ENTRY_POINT_KEY:str:entry_point} - callable_keys: ${CALLABLE_KEYS:list:["run_callable","transform_callable","evaluate_callable"]} -1: - models: - params: - args: - cleanup_freq: ${CLEANUP_FREQ:int:50} - timeout_limit: ${TIMEOUT_LIMIT:int:3} - file_hash_to_id: ${FILE_HASH_TO_ID:list:[["bafybeifpqcxwjjlenpa7n3nnx5ornrg7uz5d7h76ugzhiwk45bx3sx3cta",["sma_strategy"]]]} - component_yaml_filename: ${COMPONENT_YAML_FILENAME:str:component.yaml} - entry_point_key: ${ENTRY_POINT_KEY:str:entry_point} - callable_keys: ${CALLABLE_KEYS:list:["run_callable","transform_callable","evaluate_callable"]} -2: - models: - params: - args: - cleanup_freq: ${CLEANUP_FREQ:int:50} - timeout_limit: ${TIMEOUT_LIMIT:int:3} - file_hash_to_id: ${FILE_HASH_TO_ID:list:[["bafybeifpqcxwjjlenpa7n3nnx5ornrg7uz5d7h76ugzhiwk45bx3sx3cta",["sma_strategy"]]]} - component_yaml_filename: ${COMPONENT_YAML_FILENAME:str:component.yaml} - entry_point_key: ${ENTRY_POINT_KEY:str:entry_point} - callable_keys: ${CALLABLE_KEYS:list:["run_callable","transform_callable","evaluate_callable"]} -3: - models: - params: - args: - cleanup_freq: ${CLEANUP_FREQ:int:50} - timeout_limit: ${TIMEOUT_LIMIT:int:3} - file_hash_to_id: ${FILE_HASH_TO_ID:list:[["bafybeifpqcxwjjlenpa7n3nnx5ornrg7uz5d7h76ugzhiwk45bx3sx3cta",["sma_strategy"]]]} - component_yaml_filename: ${COMPONENT_YAML_FILENAME:str:component.yaml} - entry_point_key: ${ENTRY_POINT_KEY:str:entry_point} - callable_keys: ${CALLABLE_KEYS:list:["run_callable","transform_callable","evaluate_callable"]} ---- -public_id: valory/trader_abci:0.1.0 -type: skill -0: - models: - params: - args: - setup: &id002 - all_participants: ${ALL_PARTICIPANTS:list:[]} - safe_contract_address: ${SAFE_CONTRACT_ADDRESS:str:unknown111111111111111111111111111111111111} - consensus_threshold: ${CONSENSUS_THRESHOLD:int:null} - cleanup_history_depth: ${CLEANUP_HISTORY_DEPTH:int:1} - cleanup_history_depth_current: ${CLEANUP_HISTORY_DEPTH_CURRENT:int:null} - drand_public_key: ${DRAND_PUBLIC_KEY:str:868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31} - finalize_timeout: ${FINALIZE_TIMEOUT:float:60.0} - genesis_config: &id003 - genesis_time: ${GENESIS_TIME:str:'2023-07-12T00:00:00.000000000Z'} - chain_id: ${GENESIS_CHAIN_ID:str:chain-c4daS1} - consensus_params: - block: - max_bytes: ${BLOCK_MAX_BYTES:str:'22020096'} - max_gas: ${MAX_GAS:str:'-1'} - time_iota_ms: ${TIME_IOTA_MS:str:'1000'} - evidence: - max_age_num_blocks: ${MAX_AGE_NUM_BLOCKS:str:'100000'} - max_age_duration: ${MAX_AGE_DURATION:str:'172800000000000'} - max_bytes: ${EVIDENCE_MAX_BYTES:str:'1048576'} - validator: - pub_key_types: ${PUB_KEY_TYPES:list:["ed25519"]} - version: ${VERSION:dict:{}} - voting_power: ${VOTING_POWER:str:'10'} - init_fallback_gas: ${INIT_FALLBACK_GAS:int:0} - keeper_allowed_retries: ${KEEPER_ALLOWED_RETRIES:int:3} - keeper_timeout: ${KEEPER_TIMEOUT:float:30.0} - max_attempts: ${MAX_ATTEMPTS:int:10} - max_healthcheck: ${MAX_HEALTHCHECK:int:120} - multisend_address: ${MULTISEND_ADDRESS:str:unknown111111111111111111111111111111111111} - on_chain_service_id: ${ON_CHAIN_SERVICE_ID:int:null} - reset_tendermint_after: ${RESET_TM_AFTER:int:200} - retry_attempts: ${RETRY_ATTEMPTS:int:400} - retry_timeout: ${RETRY_TIMEOUT:int:3} - reset_pause_duration: ${RESET_PAUSE_DURATION:int:60} - request_retry_delay: ${REQUEST_RETRY_DELAY:float:1.0} - request_timeout: ${REQUEST_TIMEOUT:float:10.0} - round_timeout_seconds: ${ROUND_TIMEOUT:float:350.0} - proxy_round_timeout_seconds: ${PROXY_ROUND_TIMEOUT:float:1200.0} - service_id: ${SERVICE_ID:str:solana_trader} - service_registry_address: ${SERVICE_REGISTRY_ADDRESS:str:unknown111111111111111111111111111111111111} - agent_registry_address: ${AGENT_REGISTRY_ADDRESS:str:unknown111111111111111111111111111111111111} - share_tm_config_on_startup: ${USE_ACN:bool:false} - sleep_time: ${SLEEP_TIME:int:1} - tendermint_check_sleep_delay: ${TM_CHECK_SLEEP_DELAY:int:3} - tendermint_com_url: ${TENDERMINT_COM_URL:str:http://localhost:8080} - tendermint_max_retries: ${TM_MAX_RETRIES:int:5} - tendermint_url: ${TENDERMINT_URL:str:http://localhost:26657} - tendermint_p2p_url: ${TM_P2P_ENDPOINT_NODE_0:str:node0:26656} - termination_sleep: ${TERMINATION_SLEEP:int:900} - tx_timeout: ${TX_TIMEOUT:float:10.0} - use_termination: ${USE_TERMINATION:bool:false} - validate_timeout: ${VALIDATE_TIMEOUT:int:1205} - history_check_timeout: ${HISTORY_CHECK_TIMEOUT:int:1205} - token_symbol_whitelist: ${TOKEN_SYMBOL_WHITELIST:list:["coingecko_id=solana&address=So11111111111111111111111111111111111111112"]} - strategies_kwargs: ${STRATEGIES_KWARGS:list:[["ma_period",20],["rsi_period",14],["rsi_overbought_threshold",70],["rsi_oversold_threshold",30]]} - use_proxy_server: ${USE_PROXY_SERVER:bool:false} - expected_swap_tx_cost: ${EXPECTED_SWAP_TX_COST:int:20000000} - ipfs_fetch_retries: ${IPFS_FETCH_RETRIES:int:5} - squad_vault: ${SQUAD_VAULT:str:39Zh4C687EXLY7CT8gjCxe2hUc3krESjUsqs7A1CKD5E} - agent_balance_threshold: ${AGENT_BALANCE_THRESHOLD:int:50000000} - multisig_balance_threshold: ${MULTISIG_BALANCE_THRESHOLD:int:1000000000} - tracked_tokens: ${TRACKED_TOKENS:list:[]} - refill_action_timeout: ${REFILL_ACTION_TIMEOUT:int:10} - rpc_polling_interval: ${RPC_POLLING_INTERVAL:int:5} - epsilon: ${EPSILON:float:0.1} - sharpe_threshold: ${SHARPE_THRESHOLD:float:1.0} - ledger_ids: ${LEDGER_IDS:list:["ethereum"]} - trade_size_in_base_token: ${TRADE_SIZE_IN_BASE_TOKEN:float:0.0001} - benchmark_tool: &id004 - args: - log_dir: ${LOG_DIR:str:/benchmarks} - get_balance: &id001 - args: - url: ${SOLANA_RPC:str:replace_with_a_solana_rpc} - token_accounts: *id001 - coingecko: &id005 - args: - endpoint: ${COINGECKO_ENDPOINT:str:https://api.coingecko.com/api/v3/coins/{token_id}/market_chart?vs_currency=usd&days=1} - api_key: ${COINGECKO_API_KEY:str:null} - prices_field: ${COINGECKO_PRICES_FIELD:str:prices} - requests_per_minute: ${COINGECKO_REQUESTS_PER_MINUTE:int:5} - credits: ${COINGECKO_CREDITS:int:10000} - rate_limited_code: ${COINGECKO_RATE_LIMITED_CODE:int:429} - tx_settlement_proxy: &id006 - args: - parameters: - amount: ${TX_PROXY_SWAP_AMOUNT:int:100000000} - slippageBps: ${TX_PROXY_SLIPPAGE_BPS:int:5} - resendAmount: ${TX_PROXY_SPAM_AMOUNT:int:200} - timeoutInMs: ${TX_PROXY_VERIFICATION_TIMEOUT_MS:int:120000} - priorityFee: ${TX_PROXY_PRIORITY_FEE:int:5000000} - response_key: ${TX_PROXY_RESPONSE_KEY:str:null} - response_type: ${TX_PROXY_RESPONSE_TYPE:str:dict} - retries: ${TX_PROXY_RETRIES:int:5} - url: ${TX_PROXY_URL:str:http://localhost:3000/tx} -1: - models: - params: - args: - setup: *id002 - cleanup_history_depth: ${CLEANUP_HISTORY_DEPTH:int:1} - cleanup_history_depth_current: ${CLEANUP_HISTORY_DEPTH_CURRENT:int:null} - drand_public_key: ${DRAND_PUBLIC_KEY:str:868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31} - finalize_timeout: ${FINALIZE_TIMEOUT:float:60.0} - genesis_config: *id003 - init_fallback_gas: ${INIT_FALLBACK_GAS:int:0} - keeper_allowed_retries: ${KEEPER_ALLOWED_RETRIES:int:3} - keeper_timeout: ${KEEPER_TIMEOUT:float:30.0} - max_attempts: ${MAX_ATTEMPTS:int:10} - max_healthcheck: ${MAX_HEALTHCHECK:int:120} - multisend_address: ${MULTISEND_ADDRESS:str:unknown111111111111111111111111111111111111} - on_chain_service_id: ${ON_CHAIN_SERVICE_ID:int:null} - reset_tendermint_after: ${RESET_TM_AFTER:int:200} - retry_attempts: ${RETRY_ATTEMPTS:int:400} - retry_timeout: ${RETRY_TIMEOUT:int:3} - reset_pause_duration: ${RESET_PAUSE_DURATION:int:60} - request_retry_delay: ${REQUEST_RETRY_DELAY:float:1.0} - request_timeout: ${REQUEST_TIMEOUT:float:10.0} - round_timeout_seconds: ${ROUND_TIMEOUT:float:350.0} - proxy_round_timeout_seconds: ${PROXY_ROUND_TIMEOUT:float:1200.0} - service_id: ${SERVICE_ID:str:solana_trader} - service_registry_address: ${SERVICE_REGISTRY_ADDRESS:str:unknown111111111111111111111111111111111111} - agent_registry_address: ${AGENT_REGISTRY_ADDRESS:str:unknown111111111111111111111111111111111111} - share_tm_config_on_startup: ${USE_ACN:bool:false} - sleep_time: ${SLEEP_TIME:int:1} - tendermint_check_sleep_delay: ${TM_CHECK_SLEEP_DELAY:int:3} - tendermint_com_url: ${TENDERMINT_COM_URL:str:http://localhost:8080} - tendermint_max_retries: ${TM_MAX_RETRIES:int:5} - tendermint_url: ${TENDERMINT_URL:str:http://localhost:26657} - tendermint_p2p_url: ${TM_P2P_ENDPOINT_NODE_0:str:node0:26656} - termination_sleep: ${TERMINATION_SLEEP:int:900} - tx_timeout: ${TX_TIMEOUT:float:10.0} - use_termination: ${USE_TERMINATION:bool:false} - validate_timeout: ${VALIDATE_TIMEOUT:int:1205} - history_check_timeout: ${HISTORY_CHECK_TIMEOUT:int:1205} - token_symbol_whitelist: ${TOKEN_SYMBOL_WHITELIST:list:["coingecko_id=solana&address=So11111111111111111111111111111111111111112"]} - strategies_kwargs: ${STRATEGIES_KWARGS:list:[["ma_period",20],["rsi_period",14],["rsi_overbought_threshold",70],["rsi_oversold_threshold",30]]} - use_proxy_server: ${USE_PROXY_SERVER:bool:false} - expected_swap_tx_cost: ${EXPECTED_SWAP_TX_COST:int:20000000} - ipfs_fetch_retries: ${IPFS_FETCH_RETRIES:int:5} - squad_vault: ${SQUAD_VAULT:str:39Zh4C687EXLY7CT8gjCxe2hUc3krESjUsqs7A1CKD5E} - agent_balance_threshold: ${AGENT_BALANCE_THRESHOLD:int:50000000} - multisig_balance_threshold: ${MULTISIG_BALANCE_THRESHOLD:int:1000000000} - tracked_tokens: ${TRACKED_TOKENS:list:[]} - refill_action_timeout: ${REFILL_ACTION_TIMEOUT:int:10} - rpc_polling_interval: ${RPC_POLLING_INTERVAL:int:5} - epsilon: ${EPSILON:float:0.1} - sharpe_threshold: ${SHARPE_THRESHOLD:float:1.0} - ledger_ids: ${LEDGER_IDS:list:["ethereum"]} - trade_size_in_base_token: ${TRADE_SIZE_IN_BASE_TOKEN:float:0.0001} - benchmark_tool: *id004 - get_balance: *id001 - token_accounts: *id001 - coingecko: *id005 - tx_settlement_proxy: *id006 -2: - models: - params: - args: - setup: *id002 - cleanup_history_depth: ${CLEANUP_HISTORY_DEPTH:int:1} - cleanup_history_depth_current: ${CLEANUP_HISTORY_DEPTH_CURRENT:int:null} - drand_public_key: ${DRAND_PUBLIC_KEY:str:868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31} - finalize_timeout: ${FINALIZE_TIMEOUT:float:60.0} - genesis_config: *id003 - init_fallback_gas: ${INIT_FALLBACK_GAS:int:0} - keeper_allowed_retries: ${KEEPER_ALLOWED_RETRIES:int:3} - keeper_timeout: ${KEEPER_TIMEOUT:float:30.0} - max_attempts: ${MAX_ATTEMPTS:int:10} - max_healthcheck: ${MAX_HEALTHCHECK:int:120} - multisend_address: ${MULTISEND_ADDRESS:str:unknown111111111111111111111111111111111111} - on_chain_service_id: ${ON_CHAIN_SERVICE_ID:int:null} - reset_tendermint_after: ${RESET_TM_AFTER:int:200} - retry_attempts: ${RETRY_ATTEMPTS:int:400} - retry_timeout: ${RETRY_TIMEOUT:int:3} - reset_pause_duration: ${RESET_PAUSE_DURATION:int:60} - request_retry_delay: ${REQUEST_RETRY_DELAY:float:1.0} - request_timeout: ${REQUEST_TIMEOUT:float:10.0} - round_timeout_seconds: ${ROUND_TIMEOUT:float:350.0} - proxy_round_timeout_seconds: ${PROXY_ROUND_TIMEOUT:float:1200.0} - service_id: ${SERVICE_ID:str:solana_trader} - service_registry_address: ${SERVICE_REGISTRY_ADDRESS:str:unknown111111111111111111111111111111111111} - agent_registry_address: ${AGENT_REGISTRY_ADDRESS:str:unknown111111111111111111111111111111111111} - share_tm_config_on_startup: ${USE_ACN:bool:false} - sleep_time: ${SLEEP_TIME:int:1} - tendermint_check_sleep_delay: ${TM_CHECK_SLEEP_DELAY:int:3} - tendermint_com_url: ${TENDERMINT_COM_URL:str:http://localhost:8080} - tendermint_max_retries: ${TM_MAX_RETRIES:int:5} - tendermint_url: ${TENDERMINT_URL:str:http://localhost:26657} - tendermint_p2p_url: ${TM_P2P_ENDPOINT_NODE_0:str:node0:26656} - termination_sleep: ${TERMINATION_SLEEP:int:900} - tx_timeout: ${TX_TIMEOUT:float:10.0} - use_termination: ${USE_TERMINATION:bool:false} - validate_timeout: ${VALIDATE_TIMEOUT:int:1205} - history_check_timeout: ${HISTORY_CHECK_TIMEOUT:int:1205} - token_symbol_whitelist: ${TOKEN_SYMBOL_WHITELIST:list:["coingecko_id=solana&address=So11111111111111111111111111111111111111112"]} - strategies_kwargs: ${STRATEGIES_KWARGS:list:[["ma_period",20],["rsi_period",14],["rsi_overbought_threshold",70],["rsi_oversold_threshold",30]]} - use_proxy_server: ${USE_PROXY_SERVER:bool:false} - expected_swap_tx_cost: ${EXPECTED_SWAP_TX_COST:int:20000000} - ipfs_fetch_retries: ${IPFS_FETCH_RETRIES:int:5} - squad_vault: ${SQUAD_VAULT:str:39Zh4C687EXLY7CT8gjCxe2hUc3krESjUsqs7A1CKD5E} - agent_balance_threshold: ${AGENT_BALANCE_THRESHOLD:int:50000000} - multisig_balance_threshold: ${MULTISIG_BALANCE_THRESHOLD:int:1000000000} - tracked_tokens: ${TRACKED_TOKENS:list:[]} - refill_action_timeout: ${REFILL_ACTION_TIMEOUT:int:10} - rpc_polling_interval: ${RPC_POLLING_INTERVAL:int:5} - epsilon: ${EPSILON:float:0.1} - sharpe_threshold: ${SHARPE_THRESHOLD:float:1.0} - ledger_ids: ${LEDGER_IDS:list:["ethereum"]} - trade_size_in_base_token: ${TRADE_SIZE_IN_BASE_TOKEN:float:0.0001} - benchmark_tool: *id004 - get_balance: *id001 - token_accounts: *id001 - coingecko: *id005 - tx_settlement_proxy: *id006 -3: - models: - params: - args: - setup: *id002 - cleanup_history_depth: ${CLEANUP_HISTORY_DEPTH:int:1} - cleanup_history_depth_current: ${CLEANUP_HISTORY_DEPTH_CURRENT:int:null} - drand_public_key: ${DRAND_PUBLIC_KEY:str:868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31} - finalize_timeout: ${FINALIZE_TIMEOUT:float:60.0} - genesis_config: *id003 - init_fallback_gas: ${INIT_FALLBACK_GAS:int:0} - keeper_allowed_retries: ${KEEPER_ALLOWED_RETRIES:int:3} - keeper_timeout: ${KEEPER_TIMEOUT:float:30.0} - max_attempts: ${MAX_ATTEMPTS:int:10} - max_healthcheck: ${MAX_HEALTHCHECK:int:120} - multisend_address: ${MULTISEND_ADDRESS:str:unknown111111111111111111111111111111111111} - on_chain_service_id: ${ON_CHAIN_SERVICE_ID:int:null} - reset_tendermint_after: ${RESET_TM_AFTER:int:200} - retry_attempts: ${RETRY_ATTEMPTS:int:400} - retry_timeout: ${RETRY_TIMEOUT:int:3} - reset_pause_duration: ${RESET_PAUSE_DURATION:int:60} - request_retry_delay: ${REQUEST_RETRY_DELAY:float:1.0} - request_timeout: ${REQUEST_TIMEOUT:float:10.0} - round_timeout_seconds: ${ROUND_TIMEOUT:float:350.0} - proxy_round_timeout_seconds: ${PROXY_ROUND_TIMEOUT:float:1200.0} - service_id: ${SERVICE_ID:str:solana_trader} - service_registry_address: ${SERVICE_REGISTRY_ADDRESS:str:unknown111111111111111111111111111111111111} - agent_registry_address: ${AGENT_REGISTRY_ADDRESS:str:unknown111111111111111111111111111111111111} - share_tm_config_on_startup: ${USE_ACN:bool:false} - sleep_time: ${SLEEP_TIME:int:1} - tendermint_check_sleep_delay: ${TM_CHECK_SLEEP_DELAY:int:3} - tendermint_com_url: ${TENDERMINT_COM_URL:str:http://localhost:8080} - tendermint_max_retries: ${TM_MAX_RETRIES:int:5} - tendermint_url: ${TENDERMINT_URL:str:http://localhost:26657} - tendermint_p2p_url: ${TM_P2P_ENDPOINT_NODE_0:str:node0:26656} - termination_sleep: ${TERMINATION_SLEEP:int:900} - tx_timeout: ${TX_TIMEOUT:float:10.0} - use_termination: ${USE_TERMINATION:bool:false} - validate_timeout: ${VALIDATE_TIMEOUT:int:1205} - history_check_timeout: ${HISTORY_CHECK_TIMEOUT:int:1205} - token_symbol_whitelist: ${TOKEN_SYMBOL_WHITELIST:list:["coingecko_id=solana&address=So11111111111111111111111111111111111111112"]} - strategies_kwargs: ${STRATEGIES_KWARGS:list:[["ma_period",20],["rsi_period",14],["rsi_overbought_threshold",70],["rsi_oversold_threshold",30]]} - use_proxy_server: ${USE_PROXY_SERVER:bool:false} - expected_swap_tx_cost: ${EXPECTED_SWAP_TX_COST:int:20000000} - ipfs_fetch_retries: ${IPFS_FETCH_RETRIES:int:5} - squad_vault: ${SQUAD_VAULT:str:39Zh4C687EXLY7CT8gjCxe2hUc3krESjUsqs7A1CKD5E} - agent_balance_threshold: ${AGENT_BALANCE_THRESHOLD:int:50000000} - multisig_balance_threshold: ${MULTISIG_BALANCE_THRESHOLD:int:1000000000} - tracked_tokens: ${TRACKED_TOKENS:list:[]} - refill_action_timeout: ${REFILL_ACTION_TIMEOUT:int:10} - rpc_polling_interval: ${RPC_POLLING_INTERVAL:int:5} - epsilon: ${EPSILON:float:0.1} - sharpe_threshold: ${SHARPE_THRESHOLD:float:1.0} - ledger_ids: ${LEDGER_IDS:list:["ethereum"]} - trade_size_in_base_token: ${TRADE_SIZE_IN_BASE_TOKEN:float:0.0001} - benchmark_tool: *id004 - get_balance: *id001 - token_accounts: *id001 - coingecko: *id005 - tx_settlement_proxy: *id006 ---- -public_id: valory/ledger:0.19.0 -type: connection -0: - config: - ledger_apis: - ethereum: - address: ${RPC_0:str:http://host.docker.internal:8545} - chain_id: ${CHAIN_ID:int:1399811149} - default_gas_price_strategy: ${DEFAULT_GAS_PRICE_STRATEGY:str:eip1559} - poa_chain: ${POA_CHAIN:bool:false} -1: - config: - ledger_apis: - ethereum: - address: ${RPC_1:str:http://host.docker.internal:8545} - chain_id: ${CHAIN_ID:int:1399811149} - default_gas_price_strategy: ${DEFAULT_GAS_PRICE_STRATEGY:str:eip1559} - poa_chain: ${POA_CHAIN:bool:false} -2: - config: - ledger_apis: - ethereum: - address: ${RPC_2:str:http://host.docker.internal:8545} - chain_id: ${CHAIN_ID:int:1399811149} - default_gas_price_strategy: ${DEFAULT_GAS_PRICE_STRATEGY:str:eip1559} - poa_chain: ${POA_CHAIN:bool:false} -3: - config: - ledger_apis: - ethereum: - address: ${RPC_3:str:http://host.docker.internal:8545} - chain_id: ${CHAIN_ID:int:1399811149} - default_gas_price_strategy: ${DEFAULT_GAS_PRICE_STRATEGY:str:eip1559} - poa_chain: ${POA_CHAIN:bool:false} ---- -public_id: valory/p2p_libp2p_client:0.1.0 -type: connection -config: - nodes: - - uri: ${ACN_URI:str:acn.staging.autonolas.tech:9005} - public_key: ${ACN_NODE_PUBLIC_KEY:str:02d3a830c9d6ea1ae91936951430dee11f4662f33118b02190693be835359a9d77} -cert_requests: -- identifier: acn - ledger_id: ethereum - message_format: '{public_key}' - not_after: '2023-01-01' - not_before: '2022-01-01' - public_key: ${ACN_NODE_PUBLIC_KEY:str:02d3a830c9d6ea1ae91936951430dee11f4662f33118b02190693be835359a9d77} - save_path: .certs/acn_cosmos_11000.txt ---- -public_id: valory/http_client:0.23.0 -type: connection -config: - host: ${HTTP_CLIENT_HOST:str:127.0.0.1} - port: ${HTTP_CLIENT_PORT:int:8000} - timeout: ${HTTP_CLIENT_TIMEOUT:int:1200} ---- -public_id: eightballer/dcxt:0.1.0 -type: connection -config: - target_skill_id: valory/trader_abci:0.1.0 - exchanges: - - name: ${DCXT_EXCHANGE_NAME:str:balancer} - key_path: ${DCXT_KEY_PATH:str:ethereum_private_key.txt} - ledger_id: ${DCXT_LEDGER_ID:str:ethereum} - rpc_url: ${DCXT_RPC_URL:str:http://host.docker.internal:8545} - etherscan_api_key: ${DCXT_ETHERSCAN_API_KEY:str:null} \ No newline at end of file + save_path: .certs/acn_cosmos_11000.txt \ No newline at end of file diff --git a/packages/valory/skills/liquidity_trader_abci/behaviours.py b/packages/valory/skills/liquidity_trader_abci/behaviours.py index 71a915a..9a53fa7 100644 --- a/packages/valory/skills/liquidity_trader_abci/behaviours.py +++ b/packages/valory/skills/liquidity_trader_abci/behaviours.py @@ -91,6 +91,8 @@ PostTxSettlementRound, StakingState, SynchronizedData, + DecideAgentRound, + DecideAgentPayload, ) from packages.valory.skills.liquidity_trader_abci.strategies.simple_strategy import ( SimpleStrategyBehaviour, @@ -3354,6 +3356,34 @@ def fetch_and_log_gas_details(self): "Gas used or effective gas price not found in the response." ) +class DecideAgentBehaviour(LiquidityTraderBaseBehaviour): + """Behaviour that executes all the actions.""" + + matching_round: Type[AbstractRound] = DecideAgentRound + + def async_act(self) -> Generator: + """Async act""" + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + sender = self.context.agent_address + if self.params.agent_transition is True: + next_event = Event.MOVE_TO_NEXT_AGENT + else: + next_event = Event.DONT_MOVE_TO_NEXT_AGENT + + payload = DecideAgentPayload( + sender=sender, + content=json.dumps( + { + "event": next_event + } + ), + ) + + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + yield from self.send_a2a_transaction(payload) + yield from self.wait_until_round_end() + + self.set_done() class LiquidityTraderRoundBehaviour(AbstractRoundBehaviour): """LiquidityTraderRoundBehaviour""" @@ -3366,5 +3396,7 @@ class LiquidityTraderRoundBehaviour(AbstractRoundBehaviour): GetPositionsBehaviour, EvaluateStrategyBehaviour, DecisionMakingBehaviour, + DecideAgentBehaviour, PostTxSettlementBehaviour, ] + diff --git a/packages/valory/skills/liquidity_trader_abci/models.py b/packages/valory/skills/liquidity_trader_abci/models.py index fe6eae3..c46d656 100644 --- a/packages/valory/skills/liquidity_trader_abci/models.py +++ b/packages/valory/skills/liquidity_trader_abci/models.py @@ -254,6 +254,9 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.min_swap_amount_threshold = self._ensure( "min_swap_amount_threshold", kwargs, int ) + self.agent_transition = self._ensure( + "merge_agent", kwargs, bool + ) self.max_fee_percentage = self._ensure("max_fee_percentage", kwargs, float) self.max_gas_percentage = self._ensure("max_gas_percentage", kwargs, float) self.balancer_graphql_endpoints = json.loads( diff --git a/packages/valory/skills/liquidity_trader_abci/payloads.py b/packages/valory/skills/liquidity_trader_abci/payloads.py index 9f912ae..182122c 100644 --- a/packages/valory/skills/liquidity_trader_abci/payloads.py +++ b/packages/valory/skills/liquidity_trader_abci/payloads.py @@ -70,6 +70,12 @@ class DecisionMakingPayload(BaseTxPayload): content: str +@dataclass(frozen=True) +class DecideAgentPayload(BaseTxPayload): + """Represent a transaction payload for the DecideAgentRound.""" + + content: str + @dataclass(frozen=True) class PostTxSettlementPayload(BaseTxPayload): diff --git a/packages/valory/skills/liquidity_trader_abci/rounds.py b/packages/valory/skills/liquidity_trader_abci/rounds.py index e7d2182..6d7041d 100644 --- a/packages/valory/skills/liquidity_trader_abci/rounds.py +++ b/packages/valory/skills/liquidity_trader_abci/rounds.py @@ -70,7 +70,9 @@ class Event(Enum): VANITY_TX_EXECUTED = "vanity_tx_executed" WAIT = "wait" STAKING_KPI_NOT_MET = "staking_kpi_not_met" - STAKING_KPI_MET = "staking_kpi_met" # nosec + STAKING_KPI_MET = "staking_kpi_met" + MOVE_TO_NEXT_AGENT = "move_to_next_agent" + DONT_MOVE_TO_NEXT_AGENT = "dont_move_to_next_agent" # nosec class SynchronizedData(BaseSynchronizedData): @@ -394,6 +396,28 @@ def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: return self.synchronized_data, Event.NO_MAJORITY return None +class DecideAgentRound(CollectSameUntilThresholdRound): + """DecisionMakingRound""" + + payload_class = DecisionMakingPayload + synchronized_data_class = SynchronizedData + done_event = Event.DONE + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: + """Process the end of the block.""" + if self.threshold_reached: + # We reference all the events here to prevent the check-abciapp-specs tool from complaining + payload = json.loads(self.most_voted_payload) + event = Event(payload["event"]) + synchronized_data = cast(SynchronizedData, self.synchronized_data) + + return synchronized_data, event + + if not self.is_majority_possible( + self.collection, self.synchronized_data.nb_participants + ): + return self.synchronized_data, Event.NO_MAJORITY + return None class PostTxSettlementRound(CollectSameUntilThresholdRound): """A round that will be called after tx settlement is done.""" @@ -445,6 +469,9 @@ class FinishedTxPreparationRound(DegenerateRound): class FailedMultiplexerRound(DegenerateRound): """FailedMultiplexerRound""" +class SwitchAgentRound(DegenerateRound): + """SwitchAgentRound""" + class LiquidityTraderAbciApp(AbciApp[Event]): """LiquidityTraderAbciApp""" @@ -488,13 +515,20 @@ class LiquidityTraderAbciApp(AbciApp[Event]): Event.WAIT: FinishedEvaluateStrategyRound, }, DecisionMakingRound: { - Event.DONE: FinishedDecisionMakingRound, + Event.DONE: DecideAgentRound, Event.ERROR: FinishedDecisionMakingRound, Event.NO_MAJORITY: DecisionMakingRound, Event.ROUND_TIMEOUT: DecisionMakingRound, Event.SETTLE: FinishedTxPreparationRound, Event.UPDATE: DecisionMakingRound, }, + DecideAgentRound: { + Event.DONT_MOVE_TO_NEXT_AGENT: FinishedDecisionMakingRound, + Event.MOVE_TO_NEXT_AGENT: SwitchAgentRound, + Event.DONE: FinishedDecisionMakingRound, + Event.NO_MAJORITY: FinishedDecisionMakingRound, + + }, PostTxSettlementRound: { Event.ACTION_EXECUTED: DecisionMakingRound, Event.CHECKPOINT_TX_EXECUTED: CallCheckpointRound, diff --git a/packages/valory/skills/liquidity_trader_abci/skill.yaml b/packages/valory/skills/liquidity_trader_abci/skill.yaml index 217b966..eb519e8 100644 --- a/packages/valory/skills/liquidity_trader_abci/skill.yaml +++ b/packages/valory/skills/liquidity_trader_abci/skill.yaml @@ -173,6 +173,7 @@ models: merkl_user_rewards_url: https://api.merkl.xyz/v3/userRewards tenderly_bundle_simulation_url: https://api.tenderly.co/api/v1/account/{tenderly_account_slug}/project/{tenderly_project_slug}/simulate-bundle tenderly_access_key: access_key + agent_transition: agent_transition tenderly_account_slug: account_slug tenderly_project_slug: project_slug chain_to_chain_id_mapping: '{"optimism":10,"base":8453,"ethereum":1}' diff --git a/packages/valory/skills/optimus_abci/behaviours.py b/packages/valory/skills/optimus_abci/behaviours.py index bb88c70..3327eb0 100644 --- a/packages/valory/skills/optimus_abci/behaviours.py +++ b/packages/valory/skills/optimus_abci/behaviours.py @@ -28,6 +28,15 @@ from packages.valory.skills.liquidity_trader_abci.behaviours import ( LiquidityTraderRoundBehaviour, ) + +from packages.valory.skills.portfolio_tracker_abci.behaviours import ( + PortfolioTrackerRoundBehaviour, +) + +from packages.valory.skills.market_data_fetcher_abci.behaviours import ( + MarketDataFetcherRoundBehaviour, +) + from packages.valory.skills.optimus_abci.composition import OptimusAbciApp from packages.valory.skills.registration_abci.behaviours import ( AgentRegistrationRoundBehaviour, @@ -36,10 +45,18 @@ from packages.valory.skills.reset_pause_abci.behaviours import ( ResetPauseABCIConsensusBehaviour, ) + +from packages.valory.skills.strategy_evaluator_abci.behaviours.round_behaviour import ( + AgentStrategyEvaluatorRoundBehaviour, +) from packages.valory.skills.termination_abci.behaviours import ( BackgroundBehaviour, TerminationAbciBehaviours, ) + +from packages.valory.skills.trader_decision_maker_abci.behaviours import ( + TraderDecisionMakerRoundBehaviour, +) from packages.valory.skills.transaction_settlement_abci.behaviours import ( TransactionSettlementRoundBehaviour, ) @@ -52,6 +69,10 @@ class OptimusConsensusBehaviour(AbstractRoundBehaviour): abci_app_cls = OptimusAbciApp # type: ignore behaviours: Set[Type[BaseBehaviour]] = { *AgentRegistrationRoundBehaviour.behaviours, + *TraderDecisionMakerRoundBehaviour.behaviours, + *MarketDataFetcherRoundBehaviour.behaviours, + *PortfolioTrackerRoundBehaviour.behaviours, + *AgentStrategyEvaluatorRoundBehaviour.behaviours, *ResetPauseABCIConsensusBehaviour.behaviours, *TransactionSettlementRoundBehaviour.behaviours, *TerminationAbciBehaviours.behaviours, diff --git a/packages/valory/skills/optimus_abci/composition.py b/packages/valory/skills/optimus_abci/composition.py index c3c11b7..00dad28 100644 --- a/packages/valory/skills/optimus_abci/composition.py +++ b/packages/valory/skills/optimus_abci/composition.py @@ -20,9 +20,13 @@ """This package contains round behaviours of OptimusAbciApp.""" import packages.valory.skills.liquidity_trader_abci.rounds as LiquidityTraderAbci +import packages.valory.skills.market_data_fetcher_abci.rounds as MarketDataFetcherAbci +import packages.valory.skills.portfolio_tracker_abci.rounds as PortfolioTrackerAbci import packages.valory.skills.registration_abci.rounds as RegistrationAbci import packages.valory.skills.reset_pause_abci.rounds as ResetAndPauseAbci -import packages.valory.skills.transaction_settlement_abci.rounds as TxSettlementAbci +import packages.valory.skills.strategy_evaluator_abci.rounds as StrategyEvaluatorAbci +import packages.valory.skills.trader_decision_maker_abci.rounds as TraderDecisionMakerAbci +import packages.valory.skills.transaction_settlement_abci.rounds as TransactionSettlementAbci from packages.valory.skills.abstract_round_abci.abci_app_chain import ( AbciAppTransitionMapping, chain, @@ -37,14 +41,28 @@ abci_app_transition_mapping: AbciAppTransitionMapping = { RegistrationAbci.FinishedRegistrationRound: LiquidityTraderAbci.CallCheckpointRound, - LiquidityTraderAbci.FinishedCallCheckpointRound: TxSettlementAbci.RandomnessTransactionSubmissionRound, - LiquidityTraderAbci.FinishedCheckStakingKPIMetRound: TxSettlementAbci.RandomnessTransactionSubmissionRound, + LiquidityTraderAbci.FinishedCallCheckpointRound: TransactionSettlementAbci.RandomnessTransactionSubmissionRound, + LiquidityTraderAbci.FinishedCheckStakingKPIMetRound: TransactionSettlementAbci.RandomnessTransactionSubmissionRound, + LiquidityTraderAbci.SwitchAgentRound:TraderDecisionMakerAbci.RandomnessRound, + TraderDecisionMakerAbci.FinishedTraderDecisionMakerRound: MarketDataFetcherAbci.FetchMarketDataRound, + TraderDecisionMakerAbci.FailedTraderDecisionMakerRound: TraderDecisionMakerAbci.RandomnessRound, + MarketDataFetcherAbci.FinishedMarketFetchRound: PortfolioTrackerAbci.PortfolioTrackerRound, + MarketDataFetcherAbci.FailedMarketFetchRound: TraderDecisionMakerAbci.RandomnessRound, + PortfolioTrackerAbci.FinishedPortfolioTrackerRound: StrategyEvaluatorAbci.StrategyExecRound, + PortfolioTrackerAbci.FailedPortfolioTrackerRound: TraderDecisionMakerAbci.RandomnessRound, + StrategyEvaluatorAbci.SwapTxPreparedRound: TransactionSettlementAbci.RandomnessTransactionSubmissionRound, + StrategyEvaluatorAbci.NoMoreSwapsRound: ResetAndPauseAbci.ResetAndPauseRound, + StrategyEvaluatorAbci.StrategyExecutionFailedRound: TraderDecisionMakerAbci.RandomnessRound, + StrategyEvaluatorAbci.InstructionPreparationFailedRound: TraderDecisionMakerAbci.RandomnessRound, + StrategyEvaluatorAbci.HodlRound: ResetAndPauseAbci.ResetAndPauseRound, + StrategyEvaluatorAbci.BacktestingNegativeRound: TraderDecisionMakerAbci.RandomnessRound, + StrategyEvaluatorAbci.BacktestingFailedRound: TraderDecisionMakerAbci.RandomnessRound, LiquidityTraderAbci.FinishedDecisionMakingRound: ResetAndPauseAbci.ResetAndPauseRound, LiquidityTraderAbci.FinishedEvaluateStrategyRound: ResetAndPauseAbci.ResetAndPauseRound, - LiquidityTraderAbci.FinishedTxPreparationRound: TxSettlementAbci.RandomnessTransactionSubmissionRound, + LiquidityTraderAbci.FinishedTxPreparationRound: TransactionSettlementAbci.RandomnessTransactionSubmissionRound, LiquidityTraderAbci.FailedMultiplexerRound: ResetAndPauseAbci.ResetAndPauseRound, - TxSettlementAbci.FinishedTransactionSubmissionRound: LiquidityTraderAbci.PostTxSettlementRound, - TxSettlementAbci.FailedRound: ResetAndPauseAbci.ResetAndPauseRound, + TransactionSettlementAbci.FinishedTransactionSubmissionRound: LiquidityTraderAbci.PostTxSettlementRound, + TransactionSettlementAbci.FailedRound: ResetAndPauseAbci.ResetAndPauseRound, ResetAndPauseAbci.FinishedResetAndPauseRound: LiquidityTraderAbci.CallCheckpointRound, ResetAndPauseAbci.FinishedResetAndPauseErrorRound: RegistrationAbci.RegistrationRound, } @@ -55,11 +73,15 @@ abci_app=TerminationAbciApp, ) -OptimusAbciApp = chain( +OptimusTraderAbciApp = chain( ( RegistrationAbci.AgentRegistrationAbciApp, + TraderDecisionMakerAbci.TraderDecisionMakerAbciApp, + MarketDataFetcherAbci.MarketDataFetcherAbciApp, + PortfolioTrackerAbci.PortfolioTrackerAbciApp, + StrategyEvaluatorAbci.StrategyEvaluatorAbciApp, LiquidityTraderAbci.LiquidityTraderAbciApp, - TxSettlementAbci.TransactionSubmissionAbciApp, + TransactionSettlementAbci.TransactionSubmissionAbciApp, ResetAndPauseAbci.ResetPauseAbciApp, ), abci_app_transition_mapping, diff --git a/packages/valory/skills/optimus_abci/skill.yaml b/packages/valory/skills/optimus_abci/skill.yaml index cfefacb..c85c5ec 100644 --- a/packages/valory/skills/optimus_abci/skill.yaml +++ b/packages/valory/skills/optimus_abci/skill.yaml @@ -16,7 +16,10 @@ fingerprint: fingerprint_ignore_patterns: [] connections: [] contracts: [] -protocols: [] +protocols: +- eightballer/tickers:0.1.0:bafybeicjbpa24tla2enenmlzipqhu6grutqso74q6y7is2cpk7acub3bca +- eightballer/orders:0.1.0:bafybeibprhniaoq3y2uzc4arwwl7yws3i54ahaicrphh5gtl4xxhxqexdy +- eightballer/balances:0.1.0:bafybeieczloag3mjzvd4y2qqpbrtx6suoqjww3v7mf3dgrez5xopmo4h3m skills: - valory/abstract_round_abci:0.1.0:bafybeiajjzuh6vf23crp55humonknirvv2f4s3dmdlfzch6tc5ow52pcgm - valory/registration_abci:0.1.0:bafybeiffipsowrqrkhjoexem7ern5ob4fabgif7wa6gtlszcoaop2e3oey @@ -24,6 +27,10 @@ skills: - valory/termination_abci:0.1.0:bafybeiekkpo5qef5zaeagm3si6v45qxcojvtjqe4a5ceccvk4q7k3xi3bi - valory/liquidity_trader_abci:0.1.0:bafybeihtca6gtyjibj6wkrcdmx3fb3a3bkpdgsphwevkatagxrbqvh6fd4 - valory/transaction_settlement_abci:0.1.0:bafybeielv6eivt2z6nforq43xewl2vmpfwpdu2s2vfogobziljnwsclmlm +- valory/market_data_fetcher_abci:0.1.0:bafybeieyaop63uqw3nk2mx7nu3yvqp45eioz7rkfn5n3ocvvt3odrddoke +- valory/trader_decision_maker_abci:0.1.0:bafybeih7duxvbjevmeez4bvdpahgvefoik3hnpjq3ik5g4jaqbuw5rybtu +- valory/strategy_evaluator_abci:0.1.0:bafybeihc7pxnwmgj2wrf7awdeiu2yjyvudlmdyfkahmpkiwq7dyt5aa44u +- valory/portfolio_tracker_abci:0.1.0:bafybeictj7o35cttmhy43xi25fxyqmfhb7g2rd4yefj6hq2362xukifrpi behaviours: main: args: {} @@ -178,9 +185,44 @@ models: max_gas_percentage: 0.1 merkl_fetch_campaigns_args: '{"url":"https://api.merkl.xyz/v3/campaigns","creator":"","live":"true"}' balancer_graphql_endpoints: '{"optimism":"https://api.studio.thegraph.com/query/75376/balancer-optimism-v2/version/latest","base":"https://api.studio.thegraph.com/query/24660/balancer-base-v2/version/latest"}' + token_symbol_whitelist: [] + trading_strategy: strategy_name + strategies_kwargs: + - - extra_1 + - value + - - extra_2 + - value + use_proxy_server: false + expected_swap_tx_cost: 20000000 + ipfs_fetch_retries: 5 + squad_vault: 39Zh4C687EXLY7CT8gjCxe2hUc3krESjUsqs7A1CKD5E + agent_balance_threshold: 50000000 + multisig_balance_threshold: 1000000000 + tracked_tokens: [] + refill_action_timeout: 10 + rpc_polling_interval: 5 + epsilon: 0.1 + sharpe_threshold: 1.0 + use_solana: false + trade_size_in_base_token: 0.0001 + ledger_ids: + - optimism + exchange_ids: + ethereum: [] + optimism: [] + base: [] + base_tokens: + ethereum: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' + optimism: '0x0b2c639c533813f4aa9d7837caf62653d097ff85' + base: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913' + native_currencies: + ethereum: ETH + optimism: oETH + base: ETH class_name: Params coingecko: args: + endpoint: https://api.coingecko.com/api/v3/coins/{token_id}/market_chart?vs_currency=usd&days=1 token_price_endpoint: https://api.coingecko.com/api/v3/simple/token_price/{asset_platform_id}?contract_addresses={token_address}&vs_currencies=usd coin_price_endpoint: https://api.coingecko.com/api/v3/simple/price?ids={coin_id}&vs_currencies=usd api_key: null @@ -200,6 +242,77 @@ models: retries: 5 url: https://drand.cloudflare.com/public/latest class_name: RandomnessApi + swap_quotes: + args: + api_id: swap_quotes + headers: + Content-Type: application/json + method: GET + parameters: + amount: 100000000 + slippageBps: 5 + response_key: null + response_type: dict + retries: 5 + url: https://quote-api.jup.ag/v6/quote + class_name: SwapQuotesSpecs + swap_instructions: + args: + api_id: swap_instructions + headers: + Content-Type: application/json + method: POST + parameters: {} + response_key: null + response_type: dict + retries: 5 + url: https://quote-api.jup.ag/v6/swap-instructions + class_name: SwapInstructionsSpecs + tx_settlement_proxy: + args: + api_id: tx_settlement_proxy + headers: + Content-Type: application/json + method: POST + parameters: + amount: 100000000 + slippageBps: 5 + resendAmount: 200 + timeoutInMs: 120000 + priorityFee: 5000000 + response_key: null + response_type: dict + retries: 5 + url: http://tx_proxy:3000/tx + class_name: TxSettlementProxy + get_balance: + args: + api_id: get_balance + headers: + Content-Type: application/json + method: POST + parameters: {} + response_key: result:value + response_type: int + error_key: error:message + error_type: str + retries: 5 + url: replace_with_a_rpc + class_name: GetBalance + token_accounts: + args: + api_id: token_accounts + headers: + Content-Type: application/json + method: POST + parameters: {} + response_key: result:value + response_type: list + error_key: error:message + error_type: str + retries: 5 + url: replace_with_a_rpc + class_name: TokenAccounts requests: args: {} class_name: Requests @@ -212,6 +325,16 @@ models: tendermint_dialogues: args: {} class_name: TendermintDialogues -dependencies: {} + tickers_dialogues: + args: {} + class_name: TickersDialogues + balances_dialogues: + args: {} + class_name: BalancesDialogues + orders_dialogues: + args: {} + class_name: OrdersDialogues +dependencies: + open-aea-cli-ipfs: + version: ==1.55.0 is_abstract: false -customs: [] diff --git a/scripts/aea-config-replace.py b/scripts/aea-config-replace.py index e189fe4..5b3bcfc 100644 --- a/scripts/aea-config-replace.py +++ b/scripts/aea-config-replace.py @@ -67,6 +67,10 @@ def main() -> None: "tenderly_access_key" ] = f"${{str:{os.getenv('TENDERLY_ACCESS_KEY')}}}" + config[5]["models"]["params"]["args"][ + "agent_transition" + ] = f"${{str:{os.getenv('AGENT_TRANSITION')}}}" + config[5]["models"]["params"]["args"][ "tenderly_account_slug" ] = f"${{str:{os.getenv('TENDERLY_ACCOUNT_SLUG')}}}" From 6a9596fbba0091f8d062da21782ae4d1af65089d Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Tue, 22 Oct 2024 10:11:48 +0530 Subject: [PATCH 06/41] feat: Update agent_transition environment variable in config files --- .../valory/agents/optimus/aea-config.yaml | 143 +++++++++++++++++- 1 file changed, 141 insertions(+), 2 deletions(-) diff --git a/packages/valory/agents/optimus/aea-config.yaml b/packages/valory/agents/optimus/aea-config.yaml index 57c6b84..15bf0c9 100644 --- a/packages/valory/agents/optimus/aea-config.yaml +++ b/packages/valory/agents/optimus/aea-config.yaml @@ -8,6 +8,7 @@ fingerprint: __init__.py: bafybeigx5mdvnamsqfum5ut7htok2y5vsnu7lrvms5gfvqi7hmv7sfbo3a fingerprint_ignore_patterns: [] connections: +- eightballer/dcxt:0.1.0:bafybeide6f32midzxzo7ms7xn3xokpthxbxwqzmuiarpl5r4guhjrp623q - valory/abci:0.1.0:bafybeiejymu4ul62zx6weoibnlsrfprfpjnplhjefz6sr6izgdr4sajlnu - valory/http_client:0.23.0:bafybeihi772xgzpqeipp3fhmvpct4y6e6tpjp4sogwqrnf3wqspgeilg4u - valory/http_server:0.22.0:bafybeihpgu56ovmq4npazdbh6y6ru5i7zuv6wvdglpxavsckyih56smu7m @@ -15,6 +16,7 @@ connections: - valory/ledger:0.19.0:bafybeigntoericenpzvwejqfuc3kqzo2pscs76qoygg5dbj6f4zxusru5e - valory/p2p_libp2p_client:0.1.0:bafybeid3xg5k2ol5adflqloy75ibgljmol6xsvzvezebsg7oudxeeolz7e contracts: +- eightballer/erc_20:0.1.0:bafybeiezbnm3f5zhuj5bsc542isnlh2fki5q4nmm2vsajzps4uuoamofo4 - valory/gnosis_safe:0.1.0:bafybeib375xmvcplw7ageic2np3hq4yqeijrvd5kl7rrdnyvswats6ngmm - valory/gnosis_safe_proxy_factory:0.1.0:bafybeicpcpyurm7gxir2gnlsgzeirzomkhcbnzr5txk67zdf4mmg737rtu - valory/multisend:0.1.0:bafybeig5byt5urg2d2bsecufxe5ql7f4mezg3mekfleeh32nmuusx66p4y @@ -36,14 +38,20 @@ skills: - valory/abstract_abci:0.1.0:bafybeidz54kvxhbdmpruzguuzzq7bjg4pekjb5amqobkxoy4oqknnobopu - valory/abstract_round_abci:0.1.0:bafybeiajjzuh6vf23crp55humonknirvv2f4s3dmdlfzch6tc5ow52pcgm - valory/liquidity_trader_abci:0.1.0:bafybeihtca6gtyjibj6wkrcdmx3fb3a3bkpdgsphwevkatagxrbqvh6fd4 +- valory/market_data_fetcher_abci:0.1.0:bafybeieyaop63uqw3nk2mx7nu3yvqp45eioz7rkfn5n3ocvvt3odrddoke +- valory/strategy_evaluator_abci:0.1.0:bafybeihc7pxnwmgj2wrf7awdeiu2yjyvudlmdyfkahmpkiwq7dyt5aa44u - valory/optimus_abci:0.1.0:bafybeifjpvqz2m7qhztib4xcjpbjkuiutrot22flqclg36amvqvrp5ra3e - valory/registration_abci:0.1.0:bafybeiffipsowrqrkhjoexem7ern5ob4fabgif7wa6gtlszcoaop2e3oey - valory/reset_pause_abci:0.1.0:bafybeif4lgvbzsmzljesxbphycdv52ka7qnihyjrjpfaseclxadcmm6yiq - valory/termination_abci:0.1.0:bafybeiekkpo5qef5zaeagm3si6v45qxcojvtjqe4a5ceccvk4q7k3xi3bi - valory/transaction_settlement_abci:0.1.0:bafybeielv6eivt2z6nforq43xewl2vmpfwpdu2s2vfogobziljnwsclmlm +- valory/trader_decision_maker_abci:0.1.0:bafybeih7duxvbjevmeez4bvdpahgvefoik3hnpjq3ik5g4jaqbuw5rybtu +- valory/ipfs_package_downloader:0.1.0:bafybeid54srronvfqbvcdjgtuhmr4mbndjkpxtgzguykeg4p3wwj3zboyi +- valory/portfolio_tracker_abci:0.1.0:bafybeictj7o35cttmhy43xi25fxyqmfhb7g2rd4yefj6hq2362xukifrpi default_ledger: ethereum required_ledgers: - ethereum +- solana default_routing: {} connection_private_key_paths: {} private_key_paths: {} @@ -118,6 +126,18 @@ cert_requests: public_key: ${str:02d3a830c9d6ea1ae91936951430dee11f4662f33118b02190693be835359a9d77} save_path: .certs/acn_cosmos_11000.txt --- +public_id: valory/ipfs_package_downloader:0.1.0 +type: skill +models: + params: + args: + cleanup_freq: ${int:50} + timeout_limit: ${int:3} + file_hash_to_id: ${list:[["bafybeic2fpf5ozhkf5jgzmppmfsprqw5ayfx6spgl3owuws464n7mkhpqi",["sma_strategy"]]]} + component_yaml_filename: ${str:component.yaml} + entry_point_key: ${str:entry_point} + callable_keys: ${list:["run_callable","transform_callable","evaluate_callable"]} +--- public_id: valory/http_server:0.22.0:bafybeicblltx7ha3ulthg7bzfccuqqyjmihhrvfeztlgrlcoxhr7kf6nbq type: connection config: @@ -130,8 +150,37 @@ models: benchmark_tool: args: log_dir: ${str:/logs} + get_balance: + args: + api_id: ${str:get_balance} + headers: + Content-Type: ${str:application/json} + method: ${str:POST} + parameters: ${dict:{}} + response_key: ${str:result:value} + response_type: ${str:int} + error_key: ${str:error:message} + error_type: ${str:str} + retries: ${int:5} + url: ${str:https://api.mainnet-beta.solana.com} + + token_accounts: + args: + api_id: ${str:token_accounts} + headers: + Content-Type: ${str:application/json} + method: ${str:POST} + parameters: ${dict:{}} + response_key: ${str:result:value} + response_type: ${str:list} + error_key: ${str:error:message} + error_type: ${str:str} + retries: ${int:5} + url: ${str:https://api.mainnet-beta.solana.com} + coingecko: args: + endpoint: ${str:https://api.coingecko.com/api/v3/coins/{token_id}/market_chart?vs_currency=usd&days=1} token_price_endpoint: ${str:https://api.coingecko.com/api/v3/simple/token_price/{asset_platform_id}?contract_addresses={token_address}&vs_currencies=usd} coin_price_endpoint: ${str:https://api.coingecko.com/api/v3/simple/price?ids={coin_id}&vs_currencies=usd} api_key: ${str:null} @@ -139,7 +188,23 @@ models: credits: ${int:10000} rate_limited_code: ${int:429} chain_to_platform_id_mapping: ${str:{"optimism":"optimistic-ethereum","base":"base","ethereum":"ethereum"}} - params: + tx_settlement_proxy: + args: + api_id: ${str:tx_settlement_proxy} + headers: + Content-Type: ${str:application/json} + method: ${str:POST} + parameters: + amount: ${int:100000000} + slippageBps: ${int:5} + resendAmount: ${int:200} + timeoutInMs: ${int:120000} + priorityFee: ${int:5000000} + response_key: ${str:null} + response_type: ${str:dict} + retries: ${int:5} + url: ${str:http://localhost:3000/tx} + params: #TO_DO: Add the params for the skill from babydegen args: cleanup_history_depth: 1 cleanup_history_depth_current: null @@ -243,4 +308,78 @@ models: min_swap_amount_threshold: ${int:10} max_fee_percentage: ${float:0.02} max_gas_percentage: ${float:0.25} - balancer_graphql_endpoints: ${str:{"optimism":"https://api.studio.thegraph.com/query/75376/balancer-optimism-v2/version/latest","base":"https://api.studio.thegraph.com/query/24660/balancer-base-v2/version/latest"}} \ No newline at end of file + balancer_graphql_endpoints: ${str:{"optimism":"https://api.studio.thegraph.com/query/75376/balancer-optimism-v2/version/latest","base":"https://api.studio.thegraph.com/query/24660/balancer-base-v2/version/latest"}} + #on_chain_service_id: ${int:null} # null or 1 + token_symbol_whitelist: ${list:["coingecko_id=weth&address=7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs"]} + tracked_tokens: ${list:["7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs"]} + strategies_kwargs: ${list:[["ma_period",35]]} + use_proxy_server: ${bool:false} + expected_swap_tx_cost: ${int:20000000} + ipfs_fetch_retries: ${int:5} + squad_vault: ${str:GFWUcLp1equf4QtrRMj5RZasm8yshs4MZa2pR5Ghz54u} + agent_balance_threshold: ${int:0} + multisig_balance_threshold: ${int:0} + refill_action_timeout: ${int:10} + rpc_polling_interval: ${int:5} + epsilon: ${float:0.1} + sharpe_threshold: ${float:-10.1} + ledger_ids: ${list:["base"]} + exchange_ids: + optimism: [] + ethereum: [] + base: + - balancer + trade_size_in_base_token: ${float:0.0001} +--- +public_id: valory/p2p_libp2p_client:0.1.0 +type: connection +config: + nodes: + - uri: ${str:acn.staging.autonolas.tech:9005} + public_key: ${str:02d3a830c9d6ea1ae91936951430dee11f4662f33118b02190693be835359a9d77} +cert_requests: +- identifier: acn + ledger_id: ethereum + message_format: '{public_key}' + not_after: '2024-01-01' + not_before: '2023-01-01' + public_key: ${str:02d3a830c9d6ea1ae91936951430dee11f4662f33118b02190693be835359a9d77} + save_path: .certs/acn_cosmos_9005.txt +--- +public_id: valory/ledger:0.19.0 +type: connection +config: + ledger_apis: + ethereum: + address: ${str:https://base.blockpi.network/v1/rpc/public} + chain_id: ${int:8453} + default_gas_price_strategy: ${str:eip1559} + poa_chain: ${bool:false} + optimism: + address: ${str:https://mainnet.optimism.io} + chain_id: ${int:10} + default_gas_price_strategy: ${str:eip1559} + poa_chain: ${bool:false} + base: + address: ${str:https://base.meowrpc.com} + chain_id: ${int:8453} + default_gas_price_strategy: ${str:eip1559} + poa_chain: ${bool:false} +--- +public_id: valory/http_client:0.23.0 +type: connection +config: + host: ${str:127.0.0.1} + port: ${int:8000} + timeout: ${int:1200} +--- +public_id: eightballer/dcxt:0.1.0 +type: connection +config: + target_skill_id: eightballer/chained_dex_app:0.1.0 + exchanges: + - name: ${str:balancer} + key_path: ${str:ethereum_private_key.txt} + ledger_id: ${str:base} + rpc_url: ${str:https://base.blockpi.network/v1/rpc/public} + etherscan_api_key: ${str:YOUR_ETHERSCAN_API_KEY} \ No newline at end of file From 817c6d35dd5ecade7970eee5082a502585439728 Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Tue, 22 Oct 2024 12:32:41 +0530 Subject: [PATCH 07/41] chore: Update dependencies and packages --- .gitignore | 72 + .gitmodules | 9 + packages/packages.json | 40 +- poetry.lock | 3290 +++++++++++++++++++++++++++++----------- pyproject.toml | 40 +- 5 files changed, 2523 insertions(+), 928 deletions(-) create mode 100644 .gitmodules diff --git a/.gitignore b/.gitignore index 37831d5..859ba44 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,75 @@ keys.json ethereum_private_key.txt __pycache__/ .env + +.nix-venv +.certs +multisig_address +multisig_vault +*/keys.json +*private_key.txt* +packages/fetchai/ +packages/open_aea/ +packages/valory/connections/abci/ +packages/valory/connections/http_client/ +packages/valory/connections/ipfs/ +packages/valory/connections/ledger/ +packages/valory/connections/p2p_libp2p_client/ + +packages/valory/skills/abstract_abci/ +packages/valory/skills/abstract_round_abci/ +packages/valory/skills/registration_abci/ +packages/valory/skills/reset_pause_abci/ +packages/valory/skills/transaction_settlement_abci/ +packages/valory/skills/termination_abci/ + +packages/valory/contracts/multisend/ +packages/valory/contracts/service_registry/ +packages/valory/contracts/gnosis_safe/ +packages/valory/contracts/gnosis_safe_proxy_factory/ + +packages/valory/protocols/abci +packages/valory/protocols/acn +packages/valory/protocols/contract_api +packages/valory/protocols/http +packages/valory/protocols/ipfs +packages/valory/protocols/ledger_api +packages/valory/protocols/tendermint + +.idea +**/__pycache__/ +*.DS_Store + +.mypy_cache +/.tox/ + +keys.json +leak_report + +agent/ +solana_trader_service/ + +./solana_trader/ + +packages/valory/skills/transaction_settlement_abci/ + +solana_trader/ +!/packages/valory/agents/solana_trader/ +!/packages/valory/services/solana_trader/ + +node_modules/ +.env + +packages/open_aea/protocols/signing +packages/eightballer/protocols/order_book +packages/eightballer/protocols/ohlcv +packages/eightballer/protocols/balances +packages/eightballer/protocols/positions +packages/eightballer/protocols/spot_asset +packages/eightballer/protocols/orders +packages/eightballer/protocols/tickers +packages/eightballer/protocols/default +packages/eightballer/protocols/markets +packages/eightballer/contracts/erc_20 +packages/eightballer/contracts/spl_token +packages/eightballer/connections/dcxt diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0294686 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "third_party/balpy"] + path = third_party/balpy + url = https://github.com/8ball030/balpy.git +[submodule "third_party/multicaller"] + path = third_party/multicaller + url = https://github.com/8ball030/multicaller.git +[submodule "olas_arbitrage/aea_contracts_solana"] + path = olas_arbitrage/aea_contracts_solana + url = https://github.com/Dassy23/aea_contracts_solana.git diff --git a/packages/packages.json b/packages/packages.json index db01f2a..3bf13ca 100644 --- a/packages/packages.json +++ b/packages/packages.json @@ -1,5 +1,10 @@ { "dev": { + "custom/eightballer/rsi_strategy/0.1.0": "bafybeigbofp2nqwcxu3rlkuugpc3w6ils3u7glse7c335rddcqg56ybh34", + "custom/eightballer/sma_strategy/0.1.0": "bafybeibve7aw6oye6dc66nl2w6wxuqgxrfh5rilw64vvtfjbld6ocnbcr4", + "custom/eightballer/vwap_momentum/0.1.0": "bafybeih2uiklkrin777kzhyk5khlnj35wapw2m5zeojrhglfosm76xjhym", + "custom/valory/trend_following_strategy/0.1.0": "bafybeibejqrhpxpcszjjq6sqqknk6ja72zfiyebqmpyw32e5hjd5hanx7e", + "custom/eightballer/always_buy/0.1.0": "bafybeic2fpf5ozhkf5jgzmppmfsprqw5ayfx6spgl3owuws464n7mkhpqi", "contract/valory/balancer_weighted_pool/0.1.0": "bafybeidyjlrlq3jrbackewedwt5irokhjupxgpqfgur2ri426cap2oqt7a", "contract/valory/balancer_vault/0.1.0": "bafybeie6twptrkqddget7pjijzob2c4jqmrrtpkwombneh35xx56djz4ru", "contract/valory/erc20/0.1.0": "bafybeiav4gh7lxfnwp4f7oorkbvjxrdsgjgyhl43rgbblaugtl76zlx7vy", @@ -8,22 +13,17 @@ "contract/valory/merkl_distributor/0.1.0": "bafybeihaqsvmncuzmwv2r6iuzc5t7ur6ugdhephz7ydftypksjidpsylbq", "contract/valory/staking_token/0.1.0": "bafybeifrvtkofw5c26b3irm6izqfdpik6vpjhm6hqwcdzx333h6vhdanai", "contract/valory/staking_activity_checker/0.1.0": "bafybeibjzsi2r5b6xd4iwl4wbwldptnynryzsdpifym4mkv32ynswx22ou", - "skill/valory/liquidity_trader_abci/0.1.0": "bafybeihtca6gtyjibj6wkrcdmx3fb3a3bkpdgsphwevkatagxrbqvh6fd4", - "skill/valory/optimus_abci/0.1.0": "bafybeifjpvqz2m7qhztib4xcjpbjkuiutrot22flqclg36amvqvrp5ra3e", - "agent/valory/optimus/0.1.0": "bafybeida2scmw3qune3n6ru7tuzquuc3mxs2cfivzcncrtlj4ziadv4sqy", - "service/valory/optimus/0.1.0": "bafybeibiiuhqronhgkxjo7x5xve24lkbqom5rqcjxg7vrl6jwavfyypmhu", - "custom/eightballer/rsi_strategy/0.1.0": "bafybeigbofp2nqwcxu3rlkuugpc3w6ils3u7glse7c335rddcqg56ybh34", - "custom/eightballer/sma_strategy/0.1.0": "bafybeibve7aw6oye6dc66nl2w6wxuqgxrfh5rilw64vvtfjbld6ocnbcr4", - "custom/eightballer/vwap_momentum/0.1.0": "bafybeih2uiklkrin777kzhyk5khlnj35wapw2m5zeojrhglfosm76xjhym", - "custom/valory/trend_following_strategy/0.1.0": "bafybeibejqrhpxpcszjjq6sqqknk6ja72zfiyebqmpyw32e5hjd5hanx7e", - "custom/eightballer/always_buy/0.1.0": "bafybeic2fpf5ozhkf5jgzmppmfsprqw5ayfx6spgl3owuws464n7mkhpqi", - "skill/valory/trader_decision_maker_abci/0.1.0": "bafybeih7duxvbjevmeez4bvdpahgvefoik3hnpjq3ik5g4jaqbuw5rybtu", - "skill/valory/strategy_evaluator_abci/0.1.0": "bafybeihc7pxnwmgj2wrf7awdeiu2yjyvudlmdyfkahmpkiwq7dyt5aa44u", - "skill/valory/market_data_fetcher_abci/0.1.0": "bafybeieyaop63uqw3nk2mx7nu3yvqp45eioz7rkfn5n3ocvvt3odrddoke", + "skill/valory/liquidity_trader_abci/0.1.0": "bafybeibchfda234wvkiqayx7w2cnpxwfhmz3tct54cttp5ui6x65ppayzu", + "skill/valory/optimus_abci/0.1.0": "bafybeihfos26e7dwjhkomoh7vcvpaxzc6eyuaqgdeph3iadyxvztc2u42m", + "skill/valory/trader_decision_maker_abci/0.1.0": "bafybeicskzcqr7dd4d5ulwss5u3hsx2wvcxatugtxiuja2neobbr3tqtkm", + "skill/valory/strategy_evaluator_abci/0.1.0": "bafybeidnozzuaeipbtapipeyupus7gmoq572pxp4sek36viyz3kbre2p54", + "skill/valory/market_data_fetcher_abci/0.1.0": "bafybeiffexwacktnhihmnhxdqs2msdzbigth62oqb7ghe2bqxwfkyvs5ty", "skill/valory/trader_abci/0.1.0": "bafybeig7vluejd62szp236nzhhgaaqbhcps2qgjhz2rmwjw2hijck2bfvm", - "skill/valory/ipfs_package_downloader/0.1.0": "bafybeid54srronvfqbvcdjgtuhmr4mbndjkpxtgzguykeg4p3wwj3zboyi", - "skill/valory/portfolio_tracker_abci/0.1.0": "bafybeictj7o35cttmhy43xi25fxyqmfhb7g2rd4yefj6hq2362xukifrpi", + "skill/valory/ipfs_package_downloader/0.1.0": "bafybeieokinjosnulrsee3kbj7ly4kjfx2ub6lmwkyplgs33vxgmx3fbvm", + "skill/valory/portfolio_tracker_abci/0.1.0": "bafybeiay2lasy2mtp5fxd77wgn2ocgy5neg42mlyitltuzrr7b6yzbduoa", + "agent/valory/optimus/0.1.0": "bafybeigs3b7al7efb7gelmpizwbvqqwldjspahijxbrxbeipfp4tycenfe", "agent/valory/solana_trader/0.1.0": "bafybeiei6g2i7bntofmpduy75tuulcleewmdiww5poxszj7yohv2wd63cq", + "service/valory/optimus/0.1.0": "bafybeidl3zdiml5silxvzxb63rxbt2rzfsyzt454wp3rexdonljdvbbfzy", "service/valory/solana_trader/0.1.0": "bafybeib5tasy5wc3cjbb6k42gz4gx3ub43cd67f66iay2fkxxcuxmnuqpy" }, "third_party": { @@ -34,11 +34,23 @@ "protocol/valory/ledger_api/1.0.0": "bafybeihdk6psr4guxmbcrc26jr2cbgzpd5aljkqvpwo64bvaz7tdti2oni", "protocol/valory/acn/1.1.0": "bafybeidluaoeakae3exseupaea4i3yvvk5vivyt227xshjlffywwxzcxqe", "protocol/valory/ipfs/0.1.0": "bafybeiftxi2qhreewgsc5wevogi7yc5g6hbcbo4uiuaibauhv3nhfcdtvm", + "protocol/eightballer/order_book/0.1.0": "bafybeibtpf6fjlzfjfvwskveb7usb3bi27fbnzd5ypwm5u4oyzjnb3s6yi", + "protocol/eightballer/ohlcv/0.1.0": "bafybeibceyzlkap55isc7rcru3b3iosb2vhzz7xjt666k672bz6ejpsiyq", + "protocol/eightballer/balances/0.1.0": "bafybeiajh5vzhcofdpemm3545t3yh6g4okpwnejvbqchxapo765batiitu", + "protocol/eightballer/positions/0.1.0": "bafybeib6v2rtylru3lmri6tpgug7sgsd3imzqrpma3nuiqgjzmtdrsblaa", + "protocol/eightballer/spot_asset/0.1.0": "bafybeibrses5hkdzjtdbplkvvqfj7g64sopcdjwsstcyxujerttmpg4hxu", + "protocol/eightballer/orders/0.1.0": "bafybeibprhniaoq3y2uzc4arwwl7yws3i54ahaicrphh5gtl4xxhxqexdy", + "protocol/eightballer/tickers/0.1.0": "bafybeicjbpa24tla2enenmlzipqhu6grutqso74q6y7is2cpk7acub3bca", + "protocol/eightballer/default/0.1.0": "bafybeid2kyktyf2kqmua5dscnax7ustvht7krpwda6modxog5xrplwtmym", + "protocol/eightballer/markets/0.1.0": "bafybeibewtfadlw4kyknbjjxxjokrndea5mlrgsj7whmbfvhp5ksmnrsi4", "protocol/valory/tendermint/0.1.0": "bafybeig4mi3vmlv5zpbjbfuzcgida6j5f2nhrpedxicmrrfjweqc5r7cra", "contract/valory/service_registry/0.1.0": "bafybeihafe524ilngwzavkhwz4er56p7nyar26lfm7lrksfiqvvzo3kdcq", + "contract/eightballer/erc_20/0.1.0": "bafybeiezbnm3f5zhuj5bsc542isnlh2fki5q4nmm2vsajzps4uuoamofo4", + "contract/eightballer/spl_token/0.1.0": "bafybeicoelddoxodp3k7v5gop2hva2xvxqtosueeej2rad6huy4byxkjni", "contract/valory/gnosis_safe_proxy_factory/0.1.0": "bafybeicpcpyurm7gxir2gnlsgzeirzomkhcbnzr5txk67zdf4mmg737rtu", "contract/valory/multisend/0.1.0": "bafybeig5byt5urg2d2bsecufxe5ql7f4mezg3mekfleeh32nmuusx66p4y", "contract/valory/gnosis_safe/0.1.0": "bafybeib375xmvcplw7ageic2np3hq4yqeijrvd5kl7rrdnyvswats6ngmm", + "connection/eightballer/dcxt/0.1.0": "bafybeihes2id6qmqqybvdqfpchu6h3i5mpnhq6lztfxp52twd22iwqxacq", "connection/valory/abci/0.1.0": "bafybeiejymu4ul62zx6weoibnlsrfprfpjnplhjefz6sr6izgdr4sajlnu", "connection/valory/http_client/0.23.0": "bafybeihi772xgzpqeipp3fhmvpct4y6e6tpjp4sogwqrnf3wqspgeilg4u", "connection/valory/ipfs/0.1.0": "bafybeiegnapkvkamis47v5ioza2haerrjdzzb23rptpmcydyneas7jc2wm", diff --git a/poetry.lock b/poetry.lock index b0082ed..7bf2a4a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,113 +2,113 @@ [[package]] name = "aiohappyeyeballs" -version = "2.4.0" +version = "2.4.3" description = "Happy Eyeballs for asyncio" optional = false python-versions = ">=3.8" files = [ - {file = "aiohappyeyeballs-2.4.0-py3-none-any.whl", hash = "sha256:7ce92076e249169a13c2f49320d1967425eaf1f407522d707d59cac7628d62bd"}, - {file = "aiohappyeyeballs-2.4.0.tar.gz", hash = "sha256:55a1714f084e63d49639800f95716da97a1f173d46a16dfcfda0016abb93b6b2"}, + {file = "aiohappyeyeballs-2.4.3-py3-none-any.whl", hash = "sha256:8a7a83727b2756f394ab2895ea0765a0a8c475e3c71e98d43d76f22b4b435572"}, + {file = "aiohappyeyeballs-2.4.3.tar.gz", hash = "sha256:75cf88a15106a5002a8eb1dab212525c00d1f4c0fa96e551c9fbe6f09a621586"}, ] [[package]] name = "aiohttp" -version = "3.10.6" +version = "3.10.10" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.8" files = [ - {file = "aiohttp-3.10.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:682836fc672972cc3101cc9e30d49c5f7e8f1d010478d46119fe725a4545acfd"}, - {file = "aiohttp-3.10.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:289fa8a20018d0d5aa9e4b35d899bd51bcb80f0d5f365d9a23e30dac3b79159b"}, - {file = "aiohttp-3.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8617c96a20dd57e7e9d398ff9d04f3d11c4d28b1767273a5b1a018ada5a654d3"}, - {file = "aiohttp-3.10.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdbeff1b062751c2a2a55b171f7050fb7073633c699299d042e962aacdbe1a07"}, - {file = "aiohttp-3.10.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ea35d849cdd4a9268f910bff4497baebbc1aa3f2f625fd8ccd9ac99c860c621"}, - {file = "aiohttp-3.10.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473961b3252f3b949bb84873d6e268fb6d8aa0ccc6eb7404fa58c76a326bb8e1"}, - {file = "aiohttp-3.10.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d2665c5df629eb2f981dab244c01bfa6cdc185f4ffa026639286c4d56fafb54"}, - {file = "aiohttp-3.10.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25d92f794f1332f656e3765841fc2b7ad5c26c3f3d01e8949eeb3495691cf9f4"}, - {file = "aiohttp-3.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9bd6b2033993d5ae80883bb29b83fb2b432270bbe067c2f53cc73bb57c46065f"}, - {file = "aiohttp-3.10.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d7f408c43f5e75ea1edc152fb375e8f46ef916f545fb66d4aebcbcfad05e2796"}, - {file = "aiohttp-3.10.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:cf8b8560aa965f87bf9c13bf9fed7025993a155ca0ce8422da74bf46d18c2f5f"}, - {file = "aiohttp-3.10.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14477c4e52e2f17437b99893fd220ffe7d7ee41df5ebf931a92b8ca82e6fd094"}, - {file = "aiohttp-3.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fb138fbf9f53928e779650f5ed26d0ea1ed8b2cab67f0ea5d63afa09fdc07593"}, - {file = "aiohttp-3.10.6-cp310-cp310-win32.whl", hash = "sha256:9843d683b8756971797be171ead21511d2215a2d6e3c899c6e3107fbbe826791"}, - {file = "aiohttp-3.10.6-cp310-cp310-win_amd64.whl", hash = "sha256:f8b8e49fe02f744d38352daca1dbef462c3874900bd8166516f6ea8e82b5aacf"}, - {file = "aiohttp-3.10.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f52e54fd776ad0da1006708762213b079b154644db54bcfc62f06eaa5b896402"}, - {file = "aiohttp-3.10.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:995ab1a238fd0d19dc65f2d222e5eb064e409665c6426a3e51d5101c1979ee84"}, - {file = "aiohttp-3.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0749c4d5a08a802dd66ecdf59b2df4d76b900004017468a7bb736c3b5a3dd902"}, - {file = "aiohttp-3.10.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e05b39158f2af0e2438cc2075cfc271f4ace0c3cc4a81ec95b27a0432e161951"}, - {file = "aiohttp-3.10.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a9f196c970db2dcde4f24317e06615363349dc357cf4d7a3b0716c20ac6d7bcd"}, - {file = "aiohttp-3.10.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:47647c8af04a70e07a2462931b0eba63146a13affa697afb4ecbab9d03a480ce"}, - {file = "aiohttp-3.10.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:669c0efe7e99f6d94d63274c06344bd0e9c8daf184ce5602a29bc39e00a18720"}, - {file = "aiohttp-3.10.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9721cdd83a994225352ca84cd537760d41a9da3c0eacb3ff534747ab8fba6d0"}, - {file = "aiohttp-3.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0b82c8ebed66ce182893e7c0b6b60ba2ace45b1df104feb52380edae266a4850"}, - {file = "aiohttp-3.10.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b169f8e755e541b72e714b89a831b315bbe70db44e33fead28516c9e13d5f931"}, - {file = "aiohttp-3.10.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0be3115753baf8b4153e64f9aa7bf6c0c64af57979aa900c31f496301b374570"}, - {file = "aiohttp-3.10.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e1f80cd17d81a404b6e70ef22bfe1870bafc511728397634ad5f5efc8698df56"}, - {file = "aiohttp-3.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6419728b08fb6380c66a470d2319cafcec554c81780e2114b7e150329b9a9a7f"}, - {file = "aiohttp-3.10.6-cp311-cp311-win32.whl", hash = "sha256:bd294dcdc1afdc510bb51d35444003f14e327572877d016d576ac3b9a5888a27"}, - {file = "aiohttp-3.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:bf861da9a43d282d6dd9dcd64c23a0fccf2c5aa5cd7c32024513c8c79fb69de3"}, - {file = "aiohttp-3.10.6-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:2708baccdc62f4b1251e59c2aac725936a900081f079b88843dabcab0feeeb27"}, - {file = "aiohttp-3.10.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7475da7a5e2ccf1a1c86c8fee241e277f4874c96564d06f726d8df8e77683ef7"}, - {file = "aiohttp-3.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:02108326574ff60267b7b35b17ac5c0bbd0008ccb942ce4c48b657bb90f0b8aa"}, - {file = "aiohttp-3.10.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:029a019627b37fa9eac5c75cc54a6bb722c4ebbf5a54d8c8c0fb4dd8facf2702"}, - {file = "aiohttp-3.10.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a637d387db6fdad95e293fab5433b775fd104ae6348d2388beaaa60d08b38c4"}, - {file = "aiohttp-3.10.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dc1a16f3fc1944c61290d33c88dc3f09ba62d159b284c38c5331868425aca426"}, - {file = "aiohttp-3.10.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81b292f37969f9cc54f4643f0be7dacabf3612b3b4a65413661cf6c350226787"}, - {file = "aiohttp-3.10.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0754690a3a26e819173a34093798c155bafb21c3c640bff13be1afa1e9d421f9"}, - {file = "aiohttp-3.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:164ecd32e65467d86843dbb121a6666c3deb23b460e3f8aefdcaacae79eb718a"}, - {file = "aiohttp-3.10.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:438c5863feb761f7ca3270d48c292c334814459f61cc12bab5ba5b702d7c9e56"}, - {file = "aiohttp-3.10.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ba18573bb1de1063d222f41de64a0d3741223982dcea863b3f74646faf618ec7"}, - {file = "aiohttp-3.10.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c82a94ddec996413a905f622f3da02c4359952aab8d817c01cf9915419525e95"}, - {file = "aiohttp-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:92351aa5363fc3c1f872ca763f86730ced32b01607f0c9662b1fa711087968d0"}, - {file = "aiohttp-3.10.6-cp312-cp312-win32.whl", hash = "sha256:3e15e33bfc73fa97c228f72e05e8795e163a693fd5323549f49367c76a6e5883"}, - {file = "aiohttp-3.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:fe517113fe4d35d9072b826c3e147d63c5f808ca8167d450b4f96c520c8a1d8d"}, - {file = "aiohttp-3.10.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:482f74057ea13d387a7549d7a7ecb60e45146d15f3e58a2d93a0ad2d5a8457cd"}, - {file = "aiohttp-3.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:03fa40d1450ee5196e843315ddf74a51afc7e83d489dbfc380eecefea74158b1"}, - {file = "aiohttp-3.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e52e59ed5f4cc3a3acfe2a610f8891f216f486de54d95d6600a2c9ba1581f4d"}, - {file = "aiohttp-3.10.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b3935a22c9e41a8000d90588bed96cf395ef572dbb409be44c6219c61d900d"}, - {file = "aiohttp-3.10.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bef1480ee50f75abcfcb4b11c12de1005968ca9d0172aec4a5057ba9f2b644f"}, - {file = "aiohttp-3.10.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:671745ea7db19693ce867359d503772177f0b20fa8f6ee1e74e00449f4c4151d"}, - {file = "aiohttp-3.10.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b50b367308ca8c12e0b50cba5773bc9abe64c428d3fd2bbf5cd25aab37c77bf"}, - {file = "aiohttp-3.10.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a504d7cdb431a777d05a124fd0b21efb94498efa743103ea01b1e3136d2e4fb"}, - {file = "aiohttp-3.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66bc81361131763660b969132a22edce2c4d184978ba39614e8f8f95db5c95f8"}, - {file = "aiohttp-3.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:27cf19a38506e2e9f12fc17e55f118f04897b0a78537055d93a9de4bf3022e3d"}, - {file = "aiohttp-3.10.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3468b39f977a11271517c6925b226720e148311039a380cc9117b1e2258a721f"}, - {file = "aiohttp-3.10.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9d26da22a793dfd424be1050712a70c0afd96345245c29aced1e35dbace03413"}, - {file = "aiohttp-3.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:844d48ff9173d0b941abed8b2ea6a412f82b56d9ab1edb918c74000c15839362"}, - {file = "aiohttp-3.10.6-cp313-cp313-win32.whl", hash = "sha256:2dd56e3c43660ed3bea67fd4c5025f1ac1f9ecf6f0b991a6e5efe2e678c490c5"}, - {file = "aiohttp-3.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:c91781d969fbced1993537f45efe1213bd6fccb4b37bfae2a026e20d6fbed206"}, - {file = "aiohttp-3.10.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:4407a80bca3e694f2d2a523058e20e1f9f98a416619e04f6dc09dc910352ac8b"}, - {file = "aiohttp-3.10.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1cb045ec5961f51af3e2c08cd6fe523f07cc6e345033adee711c49b7b91bb954"}, - {file = "aiohttp-3.10.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4fabdcdc781a36b8fd7b2ca9dea8172f29a99e11d00ca0f83ffeb50958da84a1"}, - {file = "aiohttp-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a9f42efcc2681790595ab3d03c0e52d01edc23a0973ea09f0dc8d295e12b8e"}, - {file = "aiohttp-3.10.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cca776a440795db437d82c07455761c85bbcf3956221c3c23b8c93176c278ce7"}, - {file = "aiohttp-3.10.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5582de171f0898139cf51dd9fcdc79b848e28d9abd68e837f0803fc9f30807b1"}, - {file = "aiohttp-3.10.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:370e2d47575c53c817ee42a18acc34aad8da4dbdaac0a6c836d58878955f1477"}, - {file = "aiohttp-3.10.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:444d1704e2af6b30766debed9be8a795958029e552fe77551355badb1944012c"}, - {file = "aiohttp-3.10.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:40271a2a375812967401c9ca8077de9368e09a43a964f4dce0ff603301ec9358"}, - {file = "aiohttp-3.10.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f3af26f86863fad12e25395805bb0babbd49d512806af91ec9708a272b696248"}, - {file = "aiohttp-3.10.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4752df44df48fd42b80f51d6a97553b482cda1274d9dc5df214a3a1aa5d8f018"}, - {file = "aiohttp-3.10.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:2cd5290ab66cfca2f90045db2cc6434c1f4f9fbf97c9f1c316e785033782e7d2"}, - {file = "aiohttp-3.10.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3427031064b0d5c95647e6369c4aa3c556402f324a3e18107cb09517abe5f962"}, - {file = "aiohttp-3.10.6-cp38-cp38-win32.whl", hash = "sha256:614fc21e86adc28e4165a6391f851a6da6e9cbd7bb232d0df7718b453a89ee98"}, - {file = "aiohttp-3.10.6-cp38-cp38-win_amd64.whl", hash = "sha256:58c5d7318a136a3874c78717dd6de57519bc64f6363c5827c2b1cb775bea71dd"}, - {file = "aiohttp-3.10.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5db26bbca8e7968c4c977a0c640e0b9ce7224e1f4dcafa57870dc6ee28e27de6"}, - {file = "aiohttp-3.10.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3fb4216e3ec0dbc01db5ba802f02ed78ad8f07121be54eb9e918448cc3f61b7c"}, - {file = "aiohttp-3.10.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a976ef488f26e224079deb3d424f29144c6d5ba4ded313198169a8af8f47fb82"}, - {file = "aiohttp-3.10.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a86610174de8a85a920e956e2d4f9945e7da89f29a00e95ac62a4a414c4ef4e"}, - {file = "aiohttp-3.10.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:217791c6a399cc4f2e6577bb44344cba1f5714a2aebf6a0bea04cfa956658284"}, - {file = "aiohttp-3.10.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba3662d41abe2eab0eeec7ee56f33ef4e0b34858f38abf24377687f9e1fb00a5"}, - {file = "aiohttp-3.10.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4dfa5ad4bce9ca30a76117fbaa1c1decf41ebb6c18a4e098df44298941566f9"}, - {file = "aiohttp-3.10.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0009258e97502936d3bd5bf2ced15769629097d0abb81e6495fba1047824fe0"}, - {file = "aiohttp-3.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0a75d5c9fb4f06c41d029ae70ad943c3a844c40c0a769d12be4b99b04f473d3d"}, - {file = "aiohttp-3.10.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8198b7c002aae2b40b2d16bfe724b9a90bcbc9b78b2566fc96131ef4e382574d"}, - {file = "aiohttp-3.10.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4611db8c907f90fe86be112efdc2398cd7b4c8eeded5a4f0314b70fdea8feab0"}, - {file = "aiohttp-3.10.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ff99ae06eef85c7a565854826114ced72765832ee16c7e3e766c5e4c5b98d20e"}, - {file = "aiohttp-3.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7641920bdcc7cd2d3ddfb8bb9133a6c9536b09dbd49490b79e125180b2d25b93"}, - {file = "aiohttp-3.10.6-cp39-cp39-win32.whl", hash = "sha256:e2e7d5591ea868d5ec82b90bbeb366a198715672841d46281b623e23079593db"}, - {file = "aiohttp-3.10.6-cp39-cp39-win_amd64.whl", hash = "sha256:b504c08c45623bf5c7ca41be380156d925f00199b3970efd758aef4a77645feb"}, - {file = "aiohttp-3.10.6.tar.gz", hash = "sha256:d2578ef941be0c2ba58f6f421a703527d08427237ed45ecb091fed6f83305336"}, + {file = "aiohttp-3.10.10-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:be7443669ae9c016b71f402e43208e13ddf00912f47f623ee5994e12fc7d4b3f"}, + {file = "aiohttp-3.10.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7b06b7843929e41a94ea09eb1ce3927865387e3e23ebe108e0d0d09b08d25be9"}, + {file = "aiohttp-3.10.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:333cf6cf8e65f6a1e06e9eb3e643a0c515bb850d470902274239fea02033e9a8"}, + {file = "aiohttp-3.10.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:274cfa632350225ce3fdeb318c23b4a10ec25c0e2c880eff951a3842cf358ac1"}, + {file = "aiohttp-3.10.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9e5e4a85bdb56d224f412d9c98ae4cbd032cc4f3161818f692cd81766eee65a"}, + {file = "aiohttp-3.10.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b606353da03edcc71130b52388d25f9a30a126e04caef1fd637e31683033abd"}, + {file = "aiohttp-3.10.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab5a5a0c7a7991d90446a198689c0535be89bbd6b410a1f9a66688f0880ec026"}, + {file = "aiohttp-3.10.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:578a4b875af3e0daaf1ac6fa983d93e0bbfec3ead753b6d6f33d467100cdc67b"}, + {file = "aiohttp-3.10.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8105fd8a890df77b76dd3054cddf01a879fc13e8af576805d667e0fa0224c35d"}, + {file = "aiohttp-3.10.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3bcd391d083f636c06a68715e69467963d1f9600f85ef556ea82e9ef25f043f7"}, + {file = "aiohttp-3.10.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fbc6264158392bad9df19537e872d476f7c57adf718944cc1e4495cbabf38e2a"}, + {file = "aiohttp-3.10.10-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e48d5021a84d341bcaf95c8460b152cfbad770d28e5fe14a768988c461b821bc"}, + {file = "aiohttp-3.10.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2609e9ab08474702cc67b7702dbb8a80e392c54613ebe80db7e8dbdb79837c68"}, + {file = "aiohttp-3.10.10-cp310-cp310-win32.whl", hash = "sha256:84afcdea18eda514c25bc68b9af2a2b1adea7c08899175a51fe7c4fb6d551257"}, + {file = "aiohttp-3.10.10-cp310-cp310-win_amd64.whl", hash = "sha256:9c72109213eb9d3874f7ac8c0c5fa90e072d678e117d9061c06e30c85b4cf0e6"}, + {file = "aiohttp-3.10.10-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c30a0eafc89d28e7f959281b58198a9fa5e99405f716c0289b7892ca345fe45f"}, + {file = "aiohttp-3.10.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:258c5dd01afc10015866114e210fb7365f0d02d9d059c3c3415382ab633fcbcb"}, + {file = "aiohttp-3.10.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:15ecd889a709b0080f02721255b3f80bb261c2293d3c748151274dfea93ac871"}, + {file = "aiohttp-3.10.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3935f82f6f4a3820270842e90456ebad3af15810cf65932bd24da4463bc0a4c"}, + {file = "aiohttp-3.10.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:413251f6fcf552a33c981c4709a6bba37b12710982fec8e558ae944bfb2abd38"}, + {file = "aiohttp-3.10.10-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1720b4f14c78a3089562b8875b53e36b51c97c51adc53325a69b79b4b48ebcb"}, + {file = "aiohttp-3.10.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:679abe5d3858b33c2cf74faec299fda60ea9de62916e8b67e625d65bf069a3b7"}, + {file = "aiohttp-3.10.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79019094f87c9fb44f8d769e41dbb664d6e8fcfd62f665ccce36762deaa0e911"}, + {file = "aiohttp-3.10.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe2fb38c2ed905a2582948e2de560675e9dfbee94c6d5ccdb1301c6d0a5bf092"}, + {file = "aiohttp-3.10.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a3f00003de6eba42d6e94fabb4125600d6e484846dbf90ea8e48a800430cc142"}, + {file = "aiohttp-3.10.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1bbb122c557a16fafc10354b9d99ebf2f2808a660d78202f10ba9d50786384b9"}, + {file = "aiohttp-3.10.10-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:30ca7c3b94708a9d7ae76ff281b2f47d8eaf2579cd05971b5dc681db8caac6e1"}, + {file = "aiohttp-3.10.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:df9270660711670e68803107d55c2b5949c2e0f2e4896da176e1ecfc068b974a"}, + {file = "aiohttp-3.10.10-cp311-cp311-win32.whl", hash = "sha256:aafc8ee9b742ce75044ae9a4d3e60e3d918d15a4c2e08a6c3c3e38fa59b92d94"}, + {file = "aiohttp-3.10.10-cp311-cp311-win_amd64.whl", hash = "sha256:362f641f9071e5f3ee6f8e7d37d5ed0d95aae656adf4ef578313ee585b585959"}, + {file = "aiohttp-3.10.10-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9294bbb581f92770e6ed5c19559e1e99255e4ca604a22c5c6397b2f9dd3ee42c"}, + {file = "aiohttp-3.10.10-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a8fa23fe62c436ccf23ff930149c047f060c7126eae3ccea005f0483f27b2e28"}, + {file = "aiohttp-3.10.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c6a5b8c7926ba5d8545c7dd22961a107526562da31a7a32fa2456baf040939f"}, + {file = "aiohttp-3.10.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:007ec22fbc573e5eb2fb7dec4198ef8f6bf2fe4ce20020798b2eb5d0abda6138"}, + {file = "aiohttp-3.10.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9627cc1a10c8c409b5822a92d57a77f383b554463d1884008e051c32ab1b3742"}, + {file = "aiohttp-3.10.10-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:50edbcad60d8f0e3eccc68da67f37268b5144ecc34d59f27a02f9611c1d4eec7"}, + {file = "aiohttp-3.10.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a45d85cf20b5e0d0aa5a8dca27cce8eddef3292bc29d72dcad1641f4ed50aa16"}, + {file = "aiohttp-3.10.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b00807e2605f16e1e198f33a53ce3c4523114059b0c09c337209ae55e3823a8"}, + {file = "aiohttp-3.10.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f2d4324a98062be0525d16f768a03e0bbb3b9fe301ceee99611dc9a7953124e6"}, + {file = "aiohttp-3.10.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:438cd072f75bb6612f2aca29f8bd7cdf6e35e8f160bc312e49fbecab77c99e3a"}, + {file = "aiohttp-3.10.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:baa42524a82f75303f714108fea528ccacf0386af429b69fff141ffef1c534f9"}, + {file = "aiohttp-3.10.10-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a7d8d14fe962153fc681f6366bdec33d4356f98a3e3567782aac1b6e0e40109a"}, + {file = "aiohttp-3.10.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1277cd707c465cd09572a774559a3cc7c7a28802eb3a2a9472588f062097205"}, + {file = "aiohttp-3.10.10-cp312-cp312-win32.whl", hash = "sha256:59bb3c54aa420521dc4ce3cc2c3fe2ad82adf7b09403fa1f48ae45c0cbde6628"}, + {file = "aiohttp-3.10.10-cp312-cp312-win_amd64.whl", hash = "sha256:0e1b370d8007c4ae31ee6db7f9a2fe801a42b146cec80a86766e7ad5c4a259cf"}, + {file = "aiohttp-3.10.10-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ad7593bb24b2ab09e65e8a1d385606f0f47c65b5a2ae6c551db67d6653e78c28"}, + {file = "aiohttp-3.10.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1eb89d3d29adaf533588f209768a9c02e44e4baf832b08118749c5fad191781d"}, + {file = "aiohttp-3.10.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3fe407bf93533a6fa82dece0e74dbcaaf5d684e5a51862887f9eaebe6372cd79"}, + {file = "aiohttp-3.10.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50aed5155f819873d23520919e16703fc8925e509abbb1a1491b0087d1cd969e"}, + {file = "aiohttp-3.10.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f05e9727ce409358baa615dbeb9b969db94324a79b5a5cea45d39bdb01d82e6"}, + {file = "aiohttp-3.10.10-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dffb610a30d643983aeb185ce134f97f290f8935f0abccdd32c77bed9388b42"}, + {file = "aiohttp-3.10.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa6658732517ddabe22c9036479eabce6036655ba87a0224c612e1ae6af2087e"}, + {file = "aiohttp-3.10.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:741a46d58677d8c733175d7e5aa618d277cd9d880301a380fd296975a9cdd7bc"}, + {file = "aiohttp-3.10.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e00e3505cd80440f6c98c6d69269dcc2a119f86ad0a9fd70bccc59504bebd68a"}, + {file = "aiohttp-3.10.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ffe595f10566f8276b76dc3a11ae4bb7eba1aac8ddd75811736a15b0d5311414"}, + {file = "aiohttp-3.10.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdfcf6443637c148c4e1a20c48c566aa694fa5e288d34b20fcdc58507882fed3"}, + {file = "aiohttp-3.10.10-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d183cf9c797a5291e8301790ed6d053480ed94070637bfaad914dd38b0981f67"}, + {file = "aiohttp-3.10.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:77abf6665ae54000b98b3c742bc6ea1d1fb31c394bcabf8b5d2c1ac3ebfe7f3b"}, + {file = "aiohttp-3.10.10-cp313-cp313-win32.whl", hash = "sha256:4470c73c12cd9109db8277287d11f9dd98f77fc54155fc71a7738a83ffcc8ea8"}, + {file = "aiohttp-3.10.10-cp313-cp313-win_amd64.whl", hash = "sha256:486f7aabfa292719a2753c016cc3a8f8172965cabb3ea2e7f7436c7f5a22a151"}, + {file = "aiohttp-3.10.10-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:1b66ccafef7336a1e1f0e389901f60c1d920102315a56df85e49552308fc0486"}, + {file = "aiohttp-3.10.10-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:acd48d5b80ee80f9432a165c0ac8cbf9253eaddb6113269a5e18699b33958dbb"}, + {file = "aiohttp-3.10.10-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3455522392fb15ff549d92fbf4b73b559d5e43dc522588f7eb3e54c3f38beee7"}, + {file = "aiohttp-3.10.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45c3b868724137f713a38376fef8120c166d1eadd50da1855c112fe97954aed8"}, + {file = "aiohttp-3.10.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:da1dee8948d2137bb51fbb8a53cce6b1bcc86003c6b42565f008438b806cccd8"}, + {file = "aiohttp-3.10.10-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c5ce2ce7c997e1971b7184ee37deb6ea9922ef5163c6ee5aa3c274b05f9e12fa"}, + {file = "aiohttp-3.10.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28529e08fde6f12eba8677f5a8608500ed33c086f974de68cc65ab218713a59d"}, + {file = "aiohttp-3.10.10-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f7db54c7914cc99d901d93a34704833568d86c20925b2762f9fa779f9cd2e70f"}, + {file = "aiohttp-3.10.10-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:03a42ac7895406220124c88911ebee31ba8b2d24c98507f4a8bf826b2937c7f2"}, + {file = "aiohttp-3.10.10-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:7e338c0523d024fad378b376a79faff37fafb3c001872a618cde1d322400a572"}, + {file = "aiohttp-3.10.10-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:038f514fe39e235e9fef6717fbf944057bfa24f9b3db9ee551a7ecf584b5b480"}, + {file = "aiohttp-3.10.10-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:64f6c17757251e2b8d885d728b6433d9d970573586a78b78ba8929b0f41d045a"}, + {file = "aiohttp-3.10.10-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:93429602396f3383a797a2a70e5f1de5df8e35535d7806c9f91df06f297e109b"}, + {file = "aiohttp-3.10.10-cp38-cp38-win32.whl", hash = "sha256:c823bc3971c44ab93e611ab1a46b1eafeae474c0c844aff4b7474287b75fe49c"}, + {file = "aiohttp-3.10.10-cp38-cp38-win_amd64.whl", hash = "sha256:54ca74df1be3c7ca1cf7f4c971c79c2daf48d9aa65dea1a662ae18926f5bc8ce"}, + {file = "aiohttp-3.10.10-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:01948b1d570f83ee7bbf5a60ea2375a89dfb09fd419170e7f5af029510033d24"}, + {file = "aiohttp-3.10.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9fc1500fd2a952c5c8e3b29aaf7e3cc6e27e9cfc0a8819b3bce48cc1b849e4cc"}, + {file = "aiohttp-3.10.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f614ab0c76397661b90b6851a030004dac502e48260ea10f2441abd2207fbcc7"}, + {file = "aiohttp-3.10.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00819de9e45d42584bed046314c40ea7e9aea95411b38971082cad449392b08c"}, + {file = "aiohttp-3.10.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05646ebe6b94cc93407b3bf34b9eb26c20722384d068eb7339de802154d61bc5"}, + {file = "aiohttp-3.10.10-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:998f3bd3cfc95e9424a6acd7840cbdd39e45bc09ef87533c006f94ac47296090"}, + {file = "aiohttp-3.10.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9010c31cd6fa59438da4e58a7f19e4753f7f264300cd152e7f90d4602449762"}, + {file = "aiohttp-3.10.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ea7ffc6d6d6f8a11e6f40091a1040995cdff02cfc9ba4c2f30a516cb2633554"}, + {file = "aiohttp-3.10.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ef9c33cc5cbca35808f6c74be11eb7f5f6b14d2311be84a15b594bd3e58b5527"}, + {file = "aiohttp-3.10.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ce0cdc074d540265bfeb31336e678b4e37316849d13b308607efa527e981f5c2"}, + {file = "aiohttp-3.10.10-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:597a079284b7ee65ee102bc3a6ea226a37d2b96d0418cc9047490f231dc09fe8"}, + {file = "aiohttp-3.10.10-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:7789050d9e5d0c309c706953e5e8876e38662d57d45f936902e176d19f1c58ab"}, + {file = "aiohttp-3.10.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e7f8b04d83483577fd9200461b057c9f14ced334dcb053090cea1da9c8321a91"}, + {file = "aiohttp-3.10.10-cp39-cp39-win32.whl", hash = "sha256:c02a30b904282777d872266b87b20ed8cc0d1501855e27f831320f471d54d983"}, + {file = "aiohttp-3.10.10-cp39-cp39-win_amd64.whl", hash = "sha256:edfe3341033a6b53a5c522c802deb2079eee5cbfbb0af032a55064bd65c73a23"}, + {file = "aiohttp-3.10.10.tar.gz", hash = "sha256:0631dd7c9f0822cc61c88586ca76d5b5ada26538097d0f1df510b082bad3411a"}, ] [package.dependencies] @@ -137,15 +137,71 @@ files = [ [package.dependencies] frozenlist = ">=1.1.0" +[[package]] +name = "anchorpy" +version = "0.18.0" +description = "The Python Anchor client." +optional = false +python-versions = ">=3.9,<4.0" +files = [ + {file = "anchorpy-0.18.0-py3-none-any.whl", hash = "sha256:664672c2de94ed8910173fd3235d918513a927de80612db625f395bc8a0160d1"}, + {file = "anchorpy-0.18.0.tar.gz", hash = "sha256:606c49f2dba41046ba60947d19c19eb17587b490b5d1a88642722b42159aa998"}, +] + +[package.dependencies] +anchorpy-core = ">=0.1.3,<0.2.0" +based58 = ">=0.1.1,<0.2.0" +borsh-construct = ">=0.1.0,<0.2.0" +construct-typing = ">=0.5.1,<0.6.0" +jsonrpcclient = ">=4.0.1,<5.0.0" +more-itertools = ">=8.11.0,<9.0.0" +py = ">=1.11.0,<2.0.0" +pyheck = ">=0.1.4,<0.2.0" +pytest = ">=7.2.0,<8.0.0" +pytest-asyncio = ">=0.21.0,<0.22.0" +pytest-xprocess = ">=0.18.1,<0.19.0" +solana = ">=0.30.2,<0.31.0" +solders = ">=0.18.0,<0.19.0" +toml = ">=0.10.2,<0.11.0" +toolz = ">=0.11.2,<0.12.0" +websockets = ">=9.0,<11.0" +zstandard = ">=0.18.0,<0.19.0" + +[package.extras] +cli = ["autoflake (>=1.4,<2.0)", "black (>=22.3.0,<23.0.0)", "genpy (>=2021.1,<2022.0)", "ipython (>=8.0.1,<9.0.0)", "typer (==0.4.1)"] + +[[package]] +name = "anchorpy-core" +version = "0.1.3" +description = "Python bindings for Anchor Rust code" +optional = false +python-versions = ">=3.9" +files = [ + {file = "anchorpy_core-0.1.3-cp37-abi3-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:c9af073115feaab9a7fd14bc9f0d19a87650042bd430e44e9c7714b18f5aeb3a"}, + {file = "anchorpy_core-0.1.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4511a3a9a0425a84305e56087b81969d2929ac642b2c1d6fb2a500c8f987d8d3"}, + {file = "anchorpy_core-0.1.3-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f5f37b3b75e891227beeb9f6ca35505317aa41e6f5a5ba92599aa3a4ecad9226"}, + {file = "anchorpy_core-0.1.3-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3690504cd40e5ea62aa78d23de3e93a8e8929fe50a86412253f9705640ce8993"}, + {file = "anchorpy_core-0.1.3-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8db7816180720041d7a756cd87e75fc9875c4e5b2c68faf8b440053d44d1a747"}, + {file = "anchorpy_core-0.1.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41134b3c08ed405c92e247ba77e966edf6859e741ae20109bcc3d02dfd0d37fc"}, + {file = "anchorpy_core-0.1.3-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:21a6cc01fb5932b9bbfcf3ab36e924c5a5f8897a4dfad73c96c2f6e934aeddf0"}, + {file = "anchorpy_core-0.1.3-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:a6105025999e7270e0961e6c8a0436aa4218fc2d78ca056361d2aa2dffea46a3"}, + {file = "anchorpy_core-0.1.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e9fe6f2e06dd0d3b27c079121a2bc0b495c7c1dc5026e81a2a0f947d7d840e5f"}, + {file = "anchorpy_core-0.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:c952fe786c126e3c8b902d10c6d423085654001478e9a77600b105b3c66ac8ec"}, + {file = "anchorpy_core-0.1.3.tar.gz", hash = "sha256:46a328ee5b6be730311ff673ad60e8e6e40e2eea7f8254c687ecff21416a2256"}, +] + +[package.dependencies] +jsonalias = "0.1.1" + [[package]] name = "anyio" -version = "4.6.0" +version = "4.6.2.post1" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" files = [ - {file = "anyio-4.6.0-py3-none-any.whl", hash = "sha256:c7d2e9d63e31599eeb636c8c5c03a7e108d73b345f064f1c19fdc87b79036a9a"}, - {file = "anyio-4.6.0.tar.gz", hash = "sha256:137b4559cbb034c477165047febb6ff83f390fc3b20bf181c1fc0a728cb8beeb"}, + {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, + {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, ] [package.dependencies] @@ -156,7 +212,7 @@ typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.21.0b1)"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] trio = ["trio (>=0.26.1)"] [[package]] @@ -211,6 +267,29 @@ files = [ {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"}, ] +[[package]] +name = "balpy" +version = "0.0.0a84" +description = "Balancer V2 Python API" +optional = false +python-versions = ">=3.10,<4" +files = [] +develop = false + +[package.dependencies] +Cython = ">=0.29.24" +cytoolz = "0.11.2" +eth-abi = ">=2.1.1" +gql = ">=2.0.0" +jstyleson = ">=0.0.2" +multicaller = {path = "../multicaller"} +requests = ">=2.25.1" +web3 = "~6" + +[package.source] +type = "directory" +url = "third_party/balpy" + [[package]] name = "base58" version = "2.1.1" @@ -225,6 +304,31 @@ files = [ [package.extras] tests = ["PyHamcrest (>=2.0.2)", "mypy", "pytest (>=4.6)", "pytest-benchmark", "pytest-cov", "pytest-flake8"] +[[package]] +name = "based58" +version = "0.1.1" +description = "A fast Python library for Base58 and Base58Check" +optional = false +python-versions = ">=3.7" +files = [ + {file = "based58-0.1.1-cp37-abi3-macosx_10_7_x86_64.whl", hash = "sha256:745851792ce5fada615f05ec61d7f360d19c76950d1e86163b2293c63a5d43bc"}, + {file = "based58-0.1.1-cp37-abi3-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:f8448a71678bd1edc0a464033695686461ab9d6d0bc3282cb29b94f883583572"}, + {file = "based58-0.1.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:852c37206374a62c5d3ef7f6777746e2ad9106beec4551539e9538633385e613"}, + {file = "based58-0.1.1-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fb17f0aaaad0381c8b676623c870c1a56aca039e2a7c8416e65904d80a415f7"}, + {file = "based58-0.1.1-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:06f3c40b358b0c6fc6fc614c43bb11ef851b6d04e519ac1eda2833420cb43799"}, + {file = "based58-0.1.1-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a9db744be79c8087eebedbffced00c608b3ed780668ab3c59f1d16e72c84947"}, + {file = "based58-0.1.1-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0506435e98836cc16e095e0d6dc428810e0acfb44bc2f3ac3e23e051a69c0e3e"}, + {file = "based58-0.1.1-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8937e97fa8690164fd11a7c642f6d02df58facd2669ae7355e379ab77c48c924"}, + {file = "based58-0.1.1-cp37-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:14b01d91ac250300ca7f634e5bf70fb2b1b9aaa90cc14357943c7da525a35aff"}, + {file = "based58-0.1.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6c03c7f0023981c7d52fc7aad23ed1f3342819358b9b11898d693c9ef4577305"}, + {file = "based58-0.1.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:621269732454875510230b85053f462dffe7d7babecc8c553fdb488fd15810ff"}, + {file = "based58-0.1.1-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:aba18f6c869fade1d1551fe398a376440771d6ce288c54cba71b7090cf08af02"}, + {file = "based58-0.1.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ae7f17b67bf0c209da859a6b833504aa3b19dbf423cbd2369aa17e89299dc972"}, + {file = "based58-0.1.1-cp37-abi3-win32.whl", hash = "sha256:d8dece575de525c1ad889d9ab239defb7a6ceffc48f044fe6e14a408fb05bef4"}, + {file = "based58-0.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:ab85804a401a7b5a7141fbb14ef5b5f7d85288357d1d3f0085d47e616cef8f5a"}, + {file = "based58-0.1.1.tar.gz", hash = "sha256:80804b346b34196c89dc7a3dc89b6021f910f4cd75aac41d433ca1880b1672dc"}, +] + [[package]] name = "bcrypt" version = "4.2.0" @@ -278,133 +382,148 @@ files = [ [[package]] name = "bitarray" -version = "2.9.2" +version = "2.9.3" description = "efficient arrays of booleans -- C extension" optional = false python-versions = "*" files = [ - {file = "bitarray-2.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:917905de565d9576eb20f53c797c15ba88b9f4f19728acabec8d01eee1d3756a"}, - {file = "bitarray-2.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b35bfcb08b7693ab4bf9059111a6e9f14e07d57ac93cd967c420db58ab9b71e1"}, - {file = "bitarray-2.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ea1923d2e7880f9e1959e035da661767b5a2e16a45dfd57d6aa831e8b65ee1bf"}, - {file = "bitarray-2.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e0b63a565e8a311cc8348ff1262d5784df0f79d64031d546411afd5dd7ef67d"}, - {file = "bitarray-2.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf0620da2b81946d28c0b16f3e3704d38e9837d85ee4f0652816e2609aaa4fed"}, - {file = "bitarray-2.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:79a9b8b05f2876c7195a2b698c47528e86a73c61ea203394ff8e7a4434bda5c8"}, - {file = "bitarray-2.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:345c76b349ff145549652436235c5532e5bfe9db690db6f0a6ad301c62b9ef21"}, - {file = "bitarray-2.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e2936f090bf3f4d1771f44f9077ebccdbc0415d2b598d51a969afcb519df505"}, - {file = "bitarray-2.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f9346e98fc2abcef90b942973087e2462af6d3e3710e82938078d3493f7fef52"}, - {file = "bitarray-2.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e6ec283d4741befb86e8c3ea2e9ac1d17416c956d392107e45263e736954b1f7"}, - {file = "bitarray-2.9.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:962892646599529917ef26266091e4cb3077c88b93c3833a909d68dcc971c4e3"}, - {file = "bitarray-2.9.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:e8da5355d7d75a52df5b84750989e34e39919ec7e59fafc4c104cc1607ab2d31"}, - {file = "bitarray-2.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:603e7d640e54ad764d2b4da6b61e126259af84f253a20f512dd10689566e5478"}, - {file = "bitarray-2.9.2-cp310-cp310-win32.whl", hash = "sha256:f00079f8e69d75c2a417de7961a77612bb77ef46c09bc74607d86de4740771ef"}, - {file = "bitarray-2.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:1bb33673e7f7190a65f0a940c1ef63266abdb391f4a3e544a47542d40a81f536"}, - {file = "bitarray-2.9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fe71fd4b76380c2772f96f1e53a524da7063645d647a4fcd3b651bdd80ca0f2e"}, - {file = "bitarray-2.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d527172919cdea1e13994a66d9708a80c3d33dedcf2f0548e4925e600fef3a3a"}, - {file = "bitarray-2.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:052c5073bdcaa9dd10628d99d37a2f33ec09364b86dd1f6281e2d9f8d3db3060"}, - {file = "bitarray-2.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e064caa55a6ed493aca1eda06f8b3f689778bc780a75e6ad7724642ba5dc62f7"}, - {file = "bitarray-2.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:508069a04f658210fdeee85a7a0ca84db4bcc110cbb1d21f692caa13210f24a7"}, - {file = "bitarray-2.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4da73ebd537d75fa7bccfc2228fcaedea0803f21dd9d0bf0d3b67fef3c4af294"}, - {file = "bitarray-2.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cb378eaa65cd43098f11ff5d27e48ee3b956d2c00d2d6b5bfc2a09fe183be47"}, - {file = "bitarray-2.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d14c790b91f6cbcd9b718f88ed737c78939980c69ac8c7f03dd7e60040c12951"}, - {file = "bitarray-2.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7eea9318293bc0ea6447e9ebfba600a62f3428bea7e9c6d42170ae4f481dbab3"}, - {file = "bitarray-2.9.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b76ffec27c7450b8a334f967366a9ebadaea66ee43f5b530c12861b1a991f503"}, - {file = "bitarray-2.9.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:76b76a07d4ee611405045c6950a1e24c4362b6b44808d4ad6eea75e0dbc59af4"}, - {file = "bitarray-2.9.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:c7d16beeaaab15b075990cd26963d6b5b22e8c5becd131781514a00b8bdd04bd"}, - {file = "bitarray-2.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60df43e868a615c7e15117a1e1c2e5e11f48f6457280eba6ddf8fbefbec7da99"}, - {file = "bitarray-2.9.2-cp311-cp311-win32.whl", hash = "sha256:e788608ed7767b7b3bbde6d49058bccdf94df0de9ca75d13aa99020cc7e68095"}, - {file = "bitarray-2.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:a23397da092ef0a8cfe729571da64c2fc30ac18243caa82ac7c4f965087506ff"}, - {file = "bitarray-2.9.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:90e3a281ffe3897991091b7c46fca38c2675bfd4399ffe79dfeded6c52715436"}, - {file = "bitarray-2.9.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bed637b674db5e6c8a97a4a321e3e4d73e72d50b5c6b29950008a93069cc64cd"}, - {file = "bitarray-2.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e49066d251dbbe4e6e3a5c3937d85b589e40e2669ad0eef41a00f82ec17d844b"}, - {file = "bitarray-2.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4344e96642e2211fb3a50558feff682c31563a4c64529a931769d40832ca79"}, - {file = "bitarray-2.9.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aeb60962ec4813c539a59fbd4f383509c7222b62c3fb1faa76b54943a613e33a"}, - {file = "bitarray-2.9.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed0f7982f10581bb16553719e5e8f933e003f5b22f7d25a68bdb30fac630a6ff"}, - {file = "bitarray-2.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c71d1cabdeee0cdda4669168618f0e46b7dace207b29da7b63aaa1adc2b54081"}, - {file = "bitarray-2.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0ef2d0a6f1502d38d911d25609b44c6cc27bee0a4363dd295df78b075041b60"}, - {file = "bitarray-2.9.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6f71d92f533770fb027388b35b6e11988ab89242b883f48a6fe7202d238c61f8"}, - {file = "bitarray-2.9.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ba0734aa300757c924f3faf8148e1b8c247176a0ac8e16aefdf9c1eb19e868f7"}, - {file = "bitarray-2.9.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:d91406f413ccbf4af6ab5ae7bc78f772a95609f9ddd14123db36ef8c37116d95"}, - {file = "bitarray-2.9.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:87abb7f80c0a042f3fe8e5264da1a2756267450bb602110d5327b8eaff7682e7"}, - {file = "bitarray-2.9.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b558ce85579b51a2e38703877d1e93b7728a7af664dd45a34e833534f0b755d"}, - {file = "bitarray-2.9.2-cp312-cp312-win32.whl", hash = "sha256:dac2399ee2889fbdd3472bfc2ede74c34cceb1ccf29a339964281a16eb1d3188"}, - {file = "bitarray-2.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:48a30d718d1a6dfc22a49547450107abe8f4afdf2abdcbe76eb9ed88edc49498"}, - {file = "bitarray-2.9.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2c6be1b651fad8f3adb7a5aa12c65b612cd9b89530969af941844ae680f7d981"}, - {file = "bitarray-2.9.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5b399ae6ab975257ec359f03b48fc00b1c1cd109471e41903548469b8feae5c"}, - {file = "bitarray-2.9.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b3543c8a1cb286ad105f11c25d8d0f712f41c5c55f90be39f0e5a1376c7d0b0"}, - {file = "bitarray-2.9.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:03adaacb79e2fb8f483ab3a67665eec53bb3fd0cd5dbd7358741aef124688db3"}, - {file = "bitarray-2.9.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ae5b0657380d2581e13e46864d147a52c1e2bbac9f59b59c576e42fa7d10cf0"}, - {file = "bitarray-2.9.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c1f4bf6ea8eb9d7f30808c2e9894237a96650adfecbf5f3643862dc5982f89e"}, - {file = "bitarray-2.9.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a8873089be2aa15494c0f81af1209f6e1237d762c5065bc4766c1b84321e1b50"}, - {file = "bitarray-2.9.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:677e67f50e2559efc677a4366707070933ad5418b8347a603a49a070890b19bc"}, - {file = "bitarray-2.9.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:a620d8ce4ea2f1c73c6b6b1399e14cb68c6915e2be3fad5808c2998ed55b4acf"}, - {file = "bitarray-2.9.2-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:64115ccabbdbe279c24c367b629c6b1d3da9ed36c7420129e27c338a3971bfee"}, - {file = "bitarray-2.9.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:5d6fb422772e75385b76ad1c52f45a68bd4efafd8be8d0061c11877be74c4d43"}, - {file = "bitarray-2.9.2-cp36-cp36m-win32.whl", hash = "sha256:852e202875dd6dfd6139ce7ec4e98dac2b17d8d25934dc99900831e81c3adaef"}, - {file = "bitarray-2.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:7dfefdcb0dc6a3ba9936063cec65a74595571b375beabe18742b3d91d087eefd"}, - {file = "bitarray-2.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b306c4cf66912511422060f7f5e1149c8bdb404f8e00e600561b0749fdd45659"}, - {file = "bitarray-2.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a09c4f81635408e3387348f415521d4b94198c562c23330f560596a6aaa26eaf"}, - {file = "bitarray-2.9.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5361413fd2ecfdf44dc8f065177dc6aba97fa80a91b815586cb388763acf7f8d"}, - {file = "bitarray-2.9.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e8a9475d415ef1eaae7942df6f780fa4dcd48fce32825eda591a17abba869299"}, - {file = "bitarray-2.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9b87baa7bfff9a5878fcc1bffe49ecde6e647a72a64b39a69cd8a2992a43a34"}, - {file = "bitarray-2.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb6b86cfdfc503e92cb71c68766a24565359136961642504a7cc9faf936d9c88"}, - {file = "bitarray-2.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cd56b8ae87ebc71bcacbd73615098e8a8de952ecbb5785b6b4e2b07da8a06e1f"}, - {file = "bitarray-2.9.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3fa909cfd675004aed8b4cc9df352415933656e0155a6209d878b7cb615c787e"}, - {file = "bitarray-2.9.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b069ca9bf728e0c5c5b60e00a89df9af34cc170c695c3bfa3b372d8f40288efb"}, - {file = "bitarray-2.9.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:6067f2f07a7121749858c7daa93c8774325c91590b3e81a299621e347740c2ae"}, - {file = "bitarray-2.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:321841cdad1dd0f58fe62e80e9c9c7531f8ebf8be93f047401e930dc47425b1e"}, - {file = "bitarray-2.9.2-cp37-cp37m-win32.whl", hash = "sha256:54e16e32e60973bb83c315de9975bc1bcfc9bd50bb13001c31da159bc49b0ca1"}, - {file = "bitarray-2.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:f4dcadb7b8034aa3491ee8f5a69b3d9ba9d7d1e55c3cc1fc45be313e708277f8"}, - {file = "bitarray-2.9.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c8919fdbd3bb596b104388b56ae4b266eb28da1f2f7dff2e1f9334a21840fe96"}, - {file = "bitarray-2.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eb7a9d8a2e400a1026de341ad48e21670a6261a75b06df162c5c39b0d0e7c8f4"}, - {file = "bitarray-2.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6ec84668dd7b937874a2b2c293cd14ba84f37be0d196dead852e0ada9815d807"}, - {file = "bitarray-2.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2de9a31c34e543ae089fd2a5ced01292f725190e379921384f695e2d7184bd3"}, - {file = "bitarray-2.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9521f49ae121a17c0a41e5112249e6fa7f6a571245b1118de81fb86e7c1bc1ce"}, - {file = "bitarray-2.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6cc6545d6d76542aee3d18c1c9485fb7b9812b8df4ebe52c4535ec42081b48f"}, - {file = "bitarray-2.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:856bbe1616425f71c0df5ef2e8755e878d9504d5a531acba58ab4273c52c117a"}, - {file = "bitarray-2.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4bba8042ea6ab331ade91bc435d81ad72fddb098e49108610b0ce7780c14e68"}, - {file = "bitarray-2.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a035da89c959d98afc813e3c62f052690d67cfd55a36592f25d734b70de7d4b0"}, - {file = "bitarray-2.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6d70b1579da7fb71be5a841a1f965d19aca0ef27f629cfc07d06b09aafd0a333"}, - {file = "bitarray-2.9.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:405b83bed28efaae6d86b6ab287c75712ead0adbfab2a1075a1b7ab47dad4d62"}, - {file = "bitarray-2.9.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7eb8be687c50da0b397d5e0ab7ca200b5ebb639e79a9f5e285851d1944c94be9"}, - {file = "bitarray-2.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eceb551dfeaf19c609003a69a0cf8264b0efd7abc3791a11dfabf4788daf0d19"}, - {file = "bitarray-2.9.2-cp38-cp38-win32.whl", hash = "sha256:bb198c6ed1edbcdaf3d1fa3c9c9d1cdb7e179a5134ef5ee660b53cdec43b34e7"}, - {file = "bitarray-2.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:648d2f2685590b0103c67a937c2fb9e09bcc8dfb166f0c7c77bd341902a6f5b3"}, - {file = "bitarray-2.9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ea816dc8f8e65841a8bbdd30e921edffeeb6f76efe6a1eb0da147b60d539d1cf"}, - {file = "bitarray-2.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4d0e32530f941c41eddfc77600ec89b65184cb909c549336463a738fab3ed285"}, - {file = "bitarray-2.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4a22266fb416a3b6c258bf7f83c9fe531ba0b755a56986a81ad69dc0f3bcc070"}, - {file = "bitarray-2.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc6d3e80dd8239850f2604833ff3168b28909c8a9357abfed95632cccd17e3e7"}, - {file = "bitarray-2.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f135e804986b12bf14f2cd1eb86674c47dea86c4c5f0fa13c88978876b97ebe6"}, - {file = "bitarray-2.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87580c7f7d14f7ec401eda7adac1e2a25e95153e9c339872c8ae61b3208819a1"}, - {file = "bitarray-2.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64b433e26993127732ac7b66a7821b2537c3044355798de7c5fcb0af34b8296f"}, - {file = "bitarray-2.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e497c535f2a9b68c69d36631bf2dba243e05eb343b00b9c7bbdc8c601c6802d"}, - {file = "bitarray-2.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e40b3cb9fa1edb4e0175d7c06345c49c7925fe93e39ef55ecb0bc40c906b0c09"}, - {file = "bitarray-2.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f2f8692f95c9e377eb19ca519d30d1f884b02feb7e115f798de47570a359e43f"}, - {file = "bitarray-2.9.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f0b84fc50b6dbeced4fa390688c07c10a73222810fb0e08392bd1a1b8259de36"}, - {file = "bitarray-2.9.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d656ad38c942e38a470ddbce26b5020e08e1a7ea86b8fd413bb9024b5189993a"}, - {file = "bitarray-2.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6ab0f1dbfe5070db98771a56aa14797595acd45a1af9eadfb193851a270e7996"}, - {file = "bitarray-2.9.2-cp39-cp39-win32.whl", hash = "sha256:0a99b23ac845a9ea3157782c97465e6ae026fe0c7c4c1ed1d88f759fd6ea52d9"}, - {file = "bitarray-2.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:9bbcfc7c279e8d74b076e514e669b683f77b4a2a328585b3f16d4c5259c91222"}, - {file = "bitarray-2.9.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:43847799461d8ba71deb4d97b47250c2c2fb66d82cd3cb8b4caf52bb97c03034"}, - {file = "bitarray-2.9.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f44381b0a4bdf64416082f4f0e7140377ae962c0ced6f983c6d7bbfc034040"}, - {file = "bitarray-2.9.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a484061616fb4b158b80789bd3cb511f399d2116525a8b29b6334c68abc2310f"}, - {file = "bitarray-2.9.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ff9e38356cc803e06134cf8ae9758e836ccd1b793135ef3db53c7c5d71e93bc"}, - {file = "bitarray-2.9.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b44105792fbdcfbda3e26ee88786790fda409da4c71f6c2b73888108cf8f062f"}, - {file = "bitarray-2.9.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7e913098de169c7fc890638ce5e171387363eb812579e637c44261460ac00aa2"}, - {file = "bitarray-2.9.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6fe315355cdfe3ed22ef355b8bdc81a805ca4d0949d921576560e5b227a1112"}, - {file = "bitarray-2.9.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f708e91fdbe443f3bec2df394ed42328fb9b0446dff5cb4199023ac6499e09fd"}, - {file = "bitarray-2.9.2-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b7b09489b71f9f1f64c0fa0977e250ec24500767dab7383ba9912495849cadf"}, - {file = "bitarray-2.9.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:128cc3488176145b9b137fdcf54c1c201809bbb8dd30b260ee40afe915843b43"}, - {file = "bitarray-2.9.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:21f21e7f56206be346bdbda2a6bdb2165a5e6a11821f88fd4911c5a6bbbdc7e2"}, - {file = "bitarray-2.9.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f4dd3af86dd8a617eb6464622fb64ca86e61ce99b59b5c35d8cd33f9c30603d"}, - {file = "bitarray-2.9.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6465de861aff7a2559f226b37982007417eab8c3557543879987f58b453519bd"}, - {file = "bitarray-2.9.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbaf2bb71d6027152d603f1d5f31e0dfd5e50173d06f877bec484e5396d4594b"}, - {file = "bitarray-2.9.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:2f32948c86e0d230a296686db28191b67ed229756f84728847daa0c7ab7406e3"}, - {file = "bitarray-2.9.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:be94e5a685e60f9d24532af8fe5c268002e9016fa80272a94727f435de3d1003"}, - {file = "bitarray-2.9.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5cc9381fd54f3c23ae1039f977bfd6d041a5c3c1518104f616643c3a5a73b15"}, - {file = "bitarray-2.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd926e8ae4d1ed1ac4a8f37212a62886292f692bc1739fde98013bf210c2d175"}, - {file = "bitarray-2.9.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:461a3dafb9d5fda0bb3385dc507d78b1984b49da3fe4c6d56c869a54373b7008"}, - {file = "bitarray-2.9.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:393cb27fd859af5fd9c16eb26b1c59b17b390ff66b3ae5d0dd258270191baf13"}, - {file = "bitarray-2.9.2.tar.gz", hash = "sha256:a8f286a51a32323715d77755ed959f94bef13972e9a2fe71b609e40e6d27957e"}, + {file = "bitarray-2.9.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2cf5f5400636c7dda797fd681795ce63932458620fe8c40955890380acba9f62"}, + {file = "bitarray-2.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3487b4718ffa5942fab777835ee36085f8dda7ec4bd0b28433efb117f84852b6"}, + {file = "bitarray-2.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:10f44b1e4994035408bea54d7bf0aec79744cad709706bedf28091a48bb7f1a4"}, + {file = "bitarray-2.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb5c16f97c65add6535748a9c98c70e7ca79759c38a2eb990127fef72f76111a"}, + {file = "bitarray-2.9.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13dbfc42971ba84e9c4ba070f720df6570285a3f89187f07ef422efcb611c19f"}, + {file = "bitarray-2.9.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c28076acfbe7f9a5494d7ae98094a6e209c390c340938845f294818ebf5e4d3"}, + {file = "bitarray-2.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7cdd21835936d9a66477836ca23b2cb63295142cb9d9158883e2c0f1f8f6bd"}, + {file = "bitarray-2.9.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f60887ab3a46e507fa6f8544d8d4b0748da48718591dfe3fe80c62bdea60f10"}, + {file = "bitarray-2.9.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f75e1abd4a37cba3002521d3f5e2b50ef4f4a74342207cad3f52468411d5d8ba"}, + {file = "bitarray-2.9.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dc63da9695383c048b83f5ab77eab35a55bbb2e77c7b6e762eba219929b45b84"}, + {file = "bitarray-2.9.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6fe5a57b859d9bc9c2fd27c78c4b7b83158faf984202de6fb44618caeebfff10"}, + {file = "bitarray-2.9.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1fe5a37bd9441a5ecc2f6e71b43df7176fa376a542ef97484310b8b46a45649a"}, + {file = "bitarray-2.9.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8a16e42c169ca818d6a15b5dd5acd5d2a26af0fa0588e1036e0e58d01f8387d4"}, + {file = "bitarray-2.9.3-cp310-cp310-win32.whl", hash = "sha256:5e6b5e7940af3474ffaa930cd1ce8215181cbe864d6b5ddb67a15d3c15e935cd"}, + {file = "bitarray-2.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:c63dbb99ef2ab1281871678624f9c9a5f1682b826e668ce559275ec488b3fa8b"}, + {file = "bitarray-2.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:49fb93b488d180f5c84b79fe687c585a84bf0295ff035d63e09ee24ce1da0558"}, + {file = "bitarray-2.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c2944fb83bbc2aa7f29a713bc4f8c1318e54fa0d06a72bedd350a3fb4a4b91d8"}, + {file = "bitarray-2.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3612d9d3788dc62f1922c917b1539f1cdf02cecc9faef8ae213a8b36093136ca"}, + {file = "bitarray-2.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90a9300cdb7c99b1e692bb790cba8acecee1a345a83e58e28c94a0d87c522237"}, + {file = "bitarray-2.9.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1211ed66acbbb221fd7554abf4206a384d79e6192d5cb95325c5c361bbb52a74"}, + {file = "bitarray-2.9.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67757279386accf93eba76b8f97b5acf1664a3e350cbea5f300f53490f8764fd"}, + {file = "bitarray-2.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64e19c6a99c32f460c2613f797f77aa37d8e298891d00ea5355158cce80e11ec"}, + {file = "bitarray-2.9.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72734bd3775f43c5a75385730abb9f84fee6c627eb14f579de4be478f1615c8c"}, + {file = "bitarray-2.9.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a92703471b5d3316c7481bc1852f620f42f7a1b62be27f39d13694827635786f"}, + {file = "bitarray-2.9.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d5d77c81300ca430d4b195ccfbb629d6858258f541b6e96c6b11ec1563cd2681"}, + {file = "bitarray-2.9.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3ba8a29c0d091c952ced1607ce715f5e0524899f24333a493807d00f5938463d"}, + {file = "bitarray-2.9.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:418171d035b191dbe5e86cd2bfb5c3e1ae7d947edc22857a897d1c7251674ae5"}, + {file = "bitarray-2.9.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e0bd272eba256183be2a17488f9cb096d2e6d3435ecf2e28c1e0857c6d20749"}, + {file = "bitarray-2.9.3-cp311-cp311-win32.whl", hash = "sha256:cc3fd2b0637a619cf13e122bbcf4729ae214d5f25623675597e67c25f9edfe61"}, + {file = "bitarray-2.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1fc2a81a585dbe5e367682156e6350d908a56e2ffd6ca651b0af01994db596f"}, + {file = "bitarray-2.9.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dc47be026f76f1728af00dc7140cec8483fe2f0c476bbf2a59ef47865e00ff96"}, + {file = "bitarray-2.9.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:82b091742ff511cdb06f90af0d2c22e7af3dbff9b8212e2e0d88dfef6a8570b3"}, + {file = "bitarray-2.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d5edb4302a0e3a3d1d0eeb891de3c615d4cb7a446fb41c21eecdcfb29400a6f"}, + {file = "bitarray-2.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb4786c5525069c19820549dd2f42d33632bc42959ad167138bd8ee5024b922b"}, + {file = "bitarray-2.9.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bfe2de2b4df61ccb9244871a0fdf1fff83be0c1bd7187048c3cf7f81c5fe631"}, + {file = "bitarray-2.9.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31e4f69538f95d2934587d957eea0d283162322dd1af29e57122b20b8cd60f92"}, + {file = "bitarray-2.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca44908b2bc08d8995770018638d62626706864f9c599b7818225a12f3dbc2c"}, + {file = "bitarray-2.9.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:279f8de5d251ee521e365df29c927d9b5732f1ed4f373d2dbbd278fcbad94ff5"}, + {file = "bitarray-2.9.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49bb631b38431c09ecd534d56ef04264397d24d18c4ee6653c84e14ae09d92d"}, + {file = "bitarray-2.9.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:192bffc93ee9a5b6c833c98d1dcc81f5633ddd726b85e18341387d0c1d51f691"}, + {file = "bitarray-2.9.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c516cec28c6511df51d87033f40ec420324a2247469b0c989d344f4d27ea37d2"}, + {file = "bitarray-2.9.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:66241cb9a1c1db294f46cd440141e57e8242874e38f3f61877f72d92ae14768a"}, + {file = "bitarray-2.9.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ab1f0e7631110c89bea7b605c0c35832333eb9cc97e5de05d71c76d42a1858c9"}, + {file = "bitarray-2.9.3-cp312-cp312-win32.whl", hash = "sha256:42aa5bee6fe8ad3385eaf5c6585016bbc38a7b75efb52ce5c6f8e00e05237dfa"}, + {file = "bitarray-2.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:dc3fd647d845b94fac3652390866f921f914a17f3807a031c826f68dae3f43e3"}, + {file = "bitarray-2.9.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fcfcc1989e3e021a282624017b7fb754210f5332e933b1c3ebc79643727b6551"}, + {file = "bitarray-2.9.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:71b1e229a706798a9e106ca7b03d4c63455deb40b18c92950ec073a05a8f8285"}, + {file = "bitarray-2.9.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bb49556d3d505d24c942a4206ad4d0d40e89fa3016a7ea6edc994d5c08d4a8e"}, + {file = "bitarray-2.9.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4466aa1e533a59d5f7fd37219d154ec3f2ba73fce3d8a2e11080ec475bc15fb"}, + {file = "bitarray-2.9.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a9b75adc0fd0bf278bea89dc3d679d74e10d2df98d3d074b7f3d36f323138818"}, + {file = "bitarray-2.9.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701582bbbeac372b1cd8a3c9daf6c2336dc2d22e14373a6271d788bc4f2b6edc"}, + {file = "bitarray-2.9.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ea1f119668bbdbd68008031491515e84441e505163918819994b28f295f762c"}, + {file = "bitarray-2.9.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f400bc18a70bfdb073532c3054ecd78a0e64f96ff7b6140adde5b122580ec2b"}, + {file = "bitarray-2.9.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aacff5656fb3e15cede7d02903da2634d376aa928d7a81ec8df19b0724d7972a"}, + {file = "bitarray-2.9.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8a2ae42a14cbf766d4478d7101da6359b0648dd813e60eb3486ac56ad2f5add3"}, + {file = "bitarray-2.9.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:616698edb547d10f0b960cb9f2e8629c55a420dd4c2b1ab46706f49a1815621d"}, + {file = "bitarray-2.9.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f277c50ba184929dfeed39b6cf9468e3446093521b0aeb52bd54a21ca08f5473"}, + {file = "bitarray-2.9.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:661237739b385c90d8837d5e96b06de093cc6e610236977e198f88f5a979686e"}, + {file = "bitarray-2.9.3-cp313-cp313-win32.whl", hash = "sha256:68acec6c19d798051f178a1197b76f891985f683f95a4b12811b68e58b080f5a"}, + {file = "bitarray-2.9.3-cp313-cp313-win_amd64.whl", hash = "sha256:3055720afdcfd7e8f630fa16db7bed7e55c9d0a1f4756195e3b250e203f3b436"}, + {file = "bitarray-2.9.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:72bf17d0e7d8a4f645655a07999d23e42472cbf2100b8dad7ce26586075241d7"}, + {file = "bitarray-2.9.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cfd332b5f1ad8c4dc3cc79ecef33c19b42d8d8e6a39fd5c9ecb5855be0b9723"}, + {file = "bitarray-2.9.3-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5b466ef1e48f25621c9d27e95deb5e33b8656827ed8aa530b972de73870bd1f"}, + {file = "bitarray-2.9.3-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:938cf26fdaf4d0adfac82d830c025523c5d36ddead0470b735286028231c1784"}, + {file = "bitarray-2.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0f766669e768ef9a2b23ecfa710b38b6a48da3f91755113c79320b207ae255d"}, + {file = "bitarray-2.9.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b6337c0c64044f35ddfb241143244aac707a68f34ae31a71dad115f773ccc8b"}, + {file = "bitarray-2.9.3-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:731b59540167f8b2b20f69f487ecee2339fc4657059906a16cb51acac17f89c3"}, + {file = "bitarray-2.9.3-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:4feed0539a9d6432361fc4d3820eea3a81fa631d542f166cf8430aad81a971da"}, + {file = "bitarray-2.9.3-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:eb65c96a42e73f35175ec738d67992ffdf054c20abee3933cfcfa2343fa1187d"}, + {file = "bitarray-2.9.3-cp36-cp36m-musllinux_1_2_s390x.whl", hash = "sha256:4f40ceac94d182de6135759d81289683ff3e4cf0da709bc5826a7fe00d754114"}, + {file = "bitarray-2.9.3-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:5b29f7844080a281635a231a37e99f0bd6f567af6cf19f4f6d212137f99a9cdf"}, + {file = "bitarray-2.9.3-cp36-cp36m-win32.whl", hash = "sha256:947cf522a3b339b73114d12417fd848fa01303dbaa7883ced4c87688dba5637c"}, + {file = "bitarray-2.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:ea794ea60d514d68777a87a74106110db7a4bbc2c46720e67010e3071afefb95"}, + {file = "bitarray-2.9.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c7bc7cb79dcac8bdce23b305e671c06eaeffb012fa065b8c33bc51df7e1733f0"}, + {file = "bitarray-2.9.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d6380ad0f929ad9220abadd1c9b7234271c4b6ea9c753a88611d489e93a8f2e"}, + {file = "bitarray-2.9.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05f4e2451e2ad450b41ede8440e52c1fd798e81027e1dc2256292ec0787d3bf1"}, + {file = "bitarray-2.9.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7267885c98138f3707c710d5b08eedef150a3e5112c760cfe1200f3366fd7064"}, + {file = "bitarray-2.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:976957423cb41df8fe0eb811dbb53d8c5ab1ca3beec7a3ca7ff679be44a72714"}, + {file = "bitarray-2.9.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0ec5141a69f73ed6ff17ea7344d5cc166e087095bfe3661dbb42b519e76aa16"}, + {file = "bitarray-2.9.3-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:218a1b7c0652a3c1020f903ded0f9768c3719fb6d43a6e9d346e985292992d35"}, + {file = "bitarray-2.9.3-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:cf0c9ebf2df280794244e1e12ed626357506ddaa2f0d6f69efe493ae7bbf4bf7"}, + {file = "bitarray-2.9.3-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:c450a04a7e091b57d4c0bd1531648522cd0ef26913ad0e5dea0432ea29b0e5c1"}, + {file = "bitarray-2.9.3-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:a212eb89a50e32ef4969387e44a7410447dc59587615e3966d090edc338a1b85"}, + {file = "bitarray-2.9.3-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:4269232026212ee6b73379b88a578107a6b36a6182307a49d5509686c7495261"}, + {file = "bitarray-2.9.3-cp37-cp37m-win32.whl", hash = "sha256:8a0fb358e6a43f216c3fb0871e2ac14c16563aec363c23bc2fbbb18f6201285d"}, + {file = "bitarray-2.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:a8368774cdc737eec8fce6f28d0abc095fbc0edccf8fab8d29fddc264b68def9"}, + {file = "bitarray-2.9.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7d0724a4fef6ded914075a3385ea2d05afdeed567902f83490ed4e7e7e75d9bf"}, + {file = "bitarray-2.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0e11b37c6dff6f41ebc49914628824ceb8c8d6ebd0fda2ebe3c0fe0c63e8621e"}, + {file = "bitarray-2.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:085f4081d72c7468f82f722a9f113e03a1f7a4c132ef4c2a4e680c5d78b7db00"}, + {file = "bitarray-2.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b530b5fbed2900634fbc43f546e384abd72ad9c49795ff5bd6a93cac1aa9c4d8"}, + {file = "bitarray-2.9.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09ff88e4385967571146fb0d270442de39393d44198f4d108f3350cfd6486f0b"}, + {file = "bitarray-2.9.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a344bb212ddf87db4976a6711d274660a5d887da4fd3faafcdaa092152f85a6d"}, + {file = "bitarray-2.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc569c96b990f92fd5946d5b50501fee48b01a116a286d1de7961ebd9c6f06f3"}, + {file = "bitarray-2.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2fbbe7938ef8a7abe3e8519fa0578b51d2787f7171d3144e7d373551b5851fd"}, + {file = "bitarray-2.9.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0b5912fab904507b47217509b01aa903d7f98b6e725e490a7f01661f4d9a4fa7"}, + {file = "bitarray-2.9.3-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:0c836ccfca9cf60927256738ef234dfe500565492eff269610cdd1bca56801d0"}, + {file = "bitarray-2.9.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:af0e4441ebf51c18fc450962f1e201c96f444d63b17cc8dcf7c0b05111bd4486"}, + {file = "bitarray-2.9.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:9e9b57175fb6fe76d7ddd0647e06a25f6e23f4b54b5febf337c5a840ab37dc3b"}, + {file = "bitarray-2.9.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:7f7de81721ae9492926bd067007ac974692182bb83fc8f0ba330a67f37a018bd"}, + {file = "bitarray-2.9.3-cp38-cp38-win32.whl", hash = "sha256:4beafb6b6e344385480df6611fdebfcb3579bbb40636ce1ddf5e72fb744e095f"}, + {file = "bitarray-2.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:d8eaeca98900bd6f06a29cdef57999813a67d314f661d14901d71e04f4cf9f00"}, + {file = "bitarray-2.9.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:413965d9d384aef90e58b959f4a39f1d5060b145c26080297b7b4cf23cf38faa"}, + {file = "bitarray-2.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2fbb56f2bb89c3a15304a6c0ea56013dc340a98337d9bbd7fc5c21451dc05f8c"}, + {file = "bitarray-2.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b8a84f39f7885627711473872d8fc58fc7a0a1e4ecd9ddf42daf9a3643432742"}, + {file = "bitarray-2.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45147a9c8580e857c1344d15bd49d2b4387777bd582a2ede11be2ba740653f28"}, + {file = "bitarray-2.9.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed255423dc60c6b2d5c0d90c13dea2962a31929767fdf1c525ab3210269e75c5"}, + {file = "bitarray-2.9.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f5bd02671ea5c4ad52bbfe0e8e8197b6e8fa85dec1e93a4a05448c19354cc65"}, + {file = "bitarray-2.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1c99c58f044549c93fb6d4cda22678deccaed19845eaa2e6917b5b7ca058f2d"}, + {file = "bitarray-2.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:921ee87681e32e17d1849e11c96eb6a8a7edaa1269dd26831013daf8546bde05"}, + {file = "bitarray-2.9.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2ed97d8ec40c4658d9f9aa8f26cb473f44fa1dbccba3fa3fbe4a102e38c6a8d7"}, + {file = "bitarray-2.9.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9d7f7db37edb9c50c9aad6a18f2e87dd7dc5ff2a33406821804a03263fedb2ca"}, + {file = "bitarray-2.9.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:292f726cdb9efc744ed0a1d7453c44151526648148a28d9a2495cc7c7b2c62a8"}, + {file = "bitarray-2.9.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2cc94784238782a9376f307b1aa9a85ce77b6eded9f82d2fe062db7fdb02c645"}, + {file = "bitarray-2.9.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5051436b1d318f6ce0df3b2f8a60bfa66a54c1d9e8719d6cb6b448140e7061f2"}, + {file = "bitarray-2.9.3-cp39-cp39-win32.whl", hash = "sha256:a3d436c686ce59fd0b93438ed2c0e1d3e1716e56bce64b874d05b9f49f1ca5d1"}, + {file = "bitarray-2.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:f168fc45664266a560f2cb28a327041b7f69d4a7faad8ab89e0a1dd7c270a70d"}, + {file = "bitarray-2.9.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ae36787299cff41f212aee33cfe1defee13979a41552665a412b6ca3fa8f7eb8"}, + {file = "bitarray-2.9.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42afe48abb8eeb386d93e7f1165ace1dd027f136a8a31edd2b20bc57a0c071d7"}, + {file = "bitarray-2.9.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451ceecdb86bb95ae101b0d65c8c4524d692ae3666662fef8c89877ce17748c5"}, + {file = "bitarray-2.9.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4d67d3e3de2aede737b12cd75a84963700c941b77b579c14bd05517e05d7a9f"}, + {file = "bitarray-2.9.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2406d13ded84049b4238815a5821e44d6f58ba00fbb6b705b6ef8ccd88be8f03"}, + {file = "bitarray-2.9.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0db944fc2a048020fc940841ef46c0295b045d45a5a582cba69f78962a49a384"}, + {file = "bitarray-2.9.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25c603f141171a7d108773d5136d14e572c473e4cdb3fb464c39c8a138522eb2"}, + {file = "bitarray-2.9.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86c06b02705305cab0914d209caa24effda81316e2f2555a71a9aa399b75c5a5"}, + {file = "bitarray-2.9.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ddda45b24a802eaaca8f794e6267ff2b62de5fe7b900b76d6f662d95192bebf"}, + {file = "bitarray-2.9.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:81490623950d04870c6dd4d7e6df2eb68dd04eca8bec327895ebee8bbe0cc3c7"}, + {file = "bitarray-2.9.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a9e69ac6a514cc574891c24a50847022dac2fef8c3f4df530f92820a07337755"}, + {file = "bitarray-2.9.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:545c695ee69d26b41351ced4c76244d8b6225669fc0af3652ff8ed5a6b28325d"}, + {file = "bitarray-2.9.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbb2e6daabd2a64d091ac7460b0c5c5f9268199ae9a8ce32737cf5273987f1fa"}, + {file = "bitarray-2.9.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a969e5cf63144b944ee8d0a0739f53ef1ae54725b5e01258d690a8995d880526"}, + {file = "bitarray-2.9.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:73bbb9301ac9000f869c51db2cc5fcc6541985d3fcdcfe6e02f90c9e672a00be"}, + {file = "bitarray-2.9.3-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c07e346926488a85a48542d898f4168f3587ec42379fef0d18be301e08a3f27"}, + {file = "bitarray-2.9.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a26d8a14cd8ee496306f2afac34833502dd1ae826355af309333b6f252b23fe"}, + {file = "bitarray-2.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cef148ed37c892395ca182d6a235524165a9f765f4283d0a1ced891e7c43c67a"}, + {file = "bitarray-2.9.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94f35a8f0c8a50ee98a8bef9a070d0b68ecf623f20a2148cc039aba5557346a6"}, + {file = "bitarray-2.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b03207460daae828e2743874c84264e8d96a8c6156490279092b624cd5d2de08"}, + {file = "bitarray-2.9.3.tar.gz", hash = "sha256:9eff55cf189b0c37ba97156a00d640eb7392db58a8049be6f26ff2712b93fa89"}, ] [[package]] @@ -453,6 +572,21 @@ d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "borsh-construct" +version = "0.1.0" +description = "Python implementation of Borsh serialization, built on the Construct library." +optional = false +python-versions = ">=3.8.3,<4.0.0" +files = [ + {file = "borsh-construct-0.1.0.tar.gz", hash = "sha256:c916758ceba70085d8f456a1cc26991b88cb64233d347767766473b651b37263"}, + {file = "borsh_construct-0.1.0-py3-none-any.whl", hash = "sha256:f584c791e2a03f8fc36e6c13011a27bcaf028c9c54ba89cd70f485a7d1c687ed"}, +] + +[package.dependencies] +construct-typing = ">=0.5.1,<0.6.0" +sumtypes = ">=0.1a5,<0.2" + [[package]] name = "cached-property" version = "1.5.2" @@ -464,6 +598,17 @@ files = [ {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, ] +[[package]] +name = "cachetools" +version = "4.2.4" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = "~=3.5" +files = [ + {file = "cachetools-4.2.4-py3-none-any.whl", hash = "sha256:92971d3cb7d2a97efff7c7bb1657f21a8f5fb309a37530537c71b1774189f2d1"}, + {file = "cachetools-4.2.4.tar.gz", hash = "sha256:89ea6f1b638d5a73a4f9226be57ac5e4f399d22770b92355f92dcb0f7f001693"}, +] + [[package]] name = "certifi" version = "2024.8.30" @@ -556,101 +701,116 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "3.3.2" +version = "3.4.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, + {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, + {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, ] [[package]] @@ -772,6 +932,117 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "construct" +version = "2.10.68" +description = "A powerful declarative symmetric parser/builder for binary data" +optional = false +python-versions = ">=3.6" +files = [ + {file = "construct-2.10.68.tar.gz", hash = "sha256:7b2a3fd8e5f597a5aa1d614c3bd516fa065db01704c72a1efaaeec6ef23d8b45"}, +] + +[package.extras] +extras = ["arrow", "cloudpickle", "enum34", "lz4", "numpy", "ruamel.yaml"] + +[[package]] +name = "construct-typing" +version = "0.5.6" +description = "Extension for the python package 'construct' that adds typing features" +optional = false +python-versions = ">=3.7" +files = [ + {file = "construct-typing-0.5.6.tar.gz", hash = "sha256:0dc501351cd6b308f15ec54e5fe7c0fbc07cc1530a1b77b4303062a0a93c1297"}, + {file = "construct_typing-0.5.6-py3-none-any.whl", hash = "sha256:39c948329e880564e33521cba497b21b07967c465b9c9037d6334e2cffa1ced9"}, +] + +[package.dependencies] +construct = "2.10.68" + +[[package]] +name = "contourpy" +version = "1.3.0" +description = "Python library for calculating contours of 2D quadrilateral grids" +optional = false +python-versions = ">=3.9" +files = [ + {file = "contourpy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:880ea32e5c774634f9fcd46504bf9f080a41ad855f4fef54f5380f5133d343c7"}, + {file = "contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42"}, + {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f8557cbb07415a4d6fa191f20fd9d2d9eb9c0b61d1b2f52a8926e43c6e9af7"}, + {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36f965570cff02b874773c49bfe85562b47030805d7d8360748f3eca570f4cab"}, + {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cacd81e2d4b6f89c9f8a5b69b86490152ff39afc58a95af002a398273e5ce589"}, + {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69375194457ad0fad3a839b9e29aa0b0ed53bb54db1bfb6c3ae43d111c31ce41"}, + {file = "contourpy-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a52040312b1a858b5e31ef28c2e865376a386c60c0e248370bbea2d3f3b760d"}, + {file = "contourpy-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3faeb2998e4fcb256542e8a926d08da08977f7f5e62cf733f3c211c2a5586223"}, + {file = "contourpy-1.3.0-cp310-cp310-win32.whl", hash = "sha256:36e0cff201bcb17a0a8ecc7f454fe078437fa6bda730e695a92f2d9932bd507f"}, + {file = "contourpy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:87ddffef1dbe5e669b5c2440b643d3fdd8622a348fe1983fad7a0f0ccb1cd67b"}, + {file = "contourpy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fa4c02abe6c446ba70d96ece336e621efa4aecae43eaa9b030ae5fb92b309ad"}, + {file = "contourpy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:834e0cfe17ba12f79963861e0f908556b2cedd52e1f75e6578801febcc6a9f49"}, + {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbc4c3217eee163fa3984fd1567632b48d6dfd29216da3ded3d7b844a8014a66"}, + {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4865cd1d419e0c7a7bf6de1777b185eebdc51470800a9f42b9e9decf17762081"}, + {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:303c252947ab4b14c08afeb52375b26781ccd6a5ccd81abcdfc1fafd14cf93c1"}, + {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637f674226be46f6ba372fd29d9523dd977a291f66ab2a74fbeb5530bb3f445d"}, + {file = "contourpy-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76a896b2f195b57db25d6b44e7e03f221d32fe318d03ede41f8b4d9ba1bff53c"}, + {file = "contourpy-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e1fd23e9d01591bab45546c089ae89d926917a66dceb3abcf01f6105d927e2cb"}, + {file = "contourpy-1.3.0-cp311-cp311-win32.whl", hash = "sha256:d402880b84df3bec6eab53cd0cf802cae6a2ef9537e70cf75e91618a3801c20c"}, + {file = "contourpy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:6cb6cc968059db9c62cb35fbf70248f40994dfcd7aa10444bbf8b3faeb7c2d67"}, + {file = "contourpy-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:570ef7cf892f0afbe5b2ee410c507ce12e15a5fa91017a0009f79f7d93a1268f"}, + {file = "contourpy-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da84c537cb8b97d153e9fb208c221c45605f73147bd4cadd23bdae915042aad6"}, + {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be4d8425bfa755e0fd76ee1e019636ccc7c29f77a7c86b4328a9eb6a26d0639"}, + {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c0da700bf58f6e0b65312d0a5e695179a71d0163957fa381bb3c1f72972537c"}, + {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb8b141bb00fa977d9122636b16aa67d37fd40a3d8b52dd837e536d64b9a4d06"}, + {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3634b5385c6716c258d0419c46d05c8aa7dc8cb70326c9a4fb66b69ad2b52e09"}, + {file = "contourpy-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0dce35502151b6bd35027ac39ba6e5a44be13a68f55735c3612c568cac3805fd"}, + {file = "contourpy-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea348f053c645100612b333adc5983d87be69acdc6d77d3169c090d3b01dc35"}, + {file = "contourpy-1.3.0-cp312-cp312-win32.whl", hash = "sha256:90f73a5116ad1ba7174341ef3ea5c3150ddf20b024b98fb0c3b29034752c8aeb"}, + {file = "contourpy-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b11b39aea6be6764f84360fce6c82211a9db32a7c7de8fa6dd5397cf1d079c3b"}, + {file = "contourpy-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3e1c7fa44aaae40a2247e2e8e0627f4bea3dd257014764aa644f319a5f8600e3"}, + {file = "contourpy-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:364174c2a76057feef647c802652f00953b575723062560498dc7930fc9b1cb7"}, + {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32b238b3b3b649e09ce9aaf51f0c261d38644bdfa35cbaf7b263457850957a84"}, + {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d51fca85f9f7ad0b65b4b9fe800406d0d77017d7270d31ec3fb1cc07358fdea0"}, + {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:732896af21716b29ab3e988d4ce14bc5133733b85956316fb0c56355f398099b"}, + {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d73f659398a0904e125280836ae6f88ba9b178b2fed6884f3b1f95b989d2c8da"}, + {file = "contourpy-1.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c6c7c2408b7048082932cf4e641fa3b8ca848259212f51c8c59c45aa7ac18f14"}, + {file = "contourpy-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f317576606de89da6b7e0861cf6061f6146ead3528acabff9236458a6ba467f8"}, + {file = "contourpy-1.3.0-cp313-cp313-win32.whl", hash = "sha256:31cd3a85dbdf1fc002280c65caa7e2b5f65e4a973fcdf70dd2fdcb9868069294"}, + {file = "contourpy-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4553c421929ec95fb07b3aaca0fae668b2eb5a5203d1217ca7c34c063c53d087"}, + {file = "contourpy-1.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:345af746d7766821d05d72cb8f3845dfd08dd137101a2cb9b24de277d716def8"}, + {file = "contourpy-1.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3bb3808858a9dc68f6f03d319acd5f1b8a337e6cdda197f02f4b8ff67ad2057b"}, + {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:420d39daa61aab1221567b42eecb01112908b2cab7f1b4106a52caaec8d36973"}, + {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d63ee447261e963af02642ffcb864e5a2ee4cbfd78080657a9880b8b1868e18"}, + {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:167d6c890815e1dac9536dca00828b445d5d0df4d6a8c6adb4a7ec3166812fa8"}, + {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:710a26b3dc80c0e4febf04555de66f5fd17e9cf7170a7b08000601a10570bda6"}, + {file = "contourpy-1.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:75ee7cb1a14c617f34a51d11fa7524173e56551646828353c4af859c56b766e2"}, + {file = "contourpy-1.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:33c92cdae89ec5135d036e7218e69b0bb2851206077251f04a6c4e0e21f03927"}, + {file = "contourpy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a11077e395f67ffc2c44ec2418cfebed032cd6da3022a94fc227b6faf8e2acb8"}, + {file = "contourpy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8134301d7e204c88ed7ab50028ba06c683000040ede1d617298611f9dc6240c"}, + {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12968fdfd5bb45ffdf6192a590bd8ddd3ba9e58360b29683c6bb71a7b41edca"}, + {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd2a0fc506eccaaa7595b7e1418951f213cf8255be2600f1ea1b61e46a60c55f"}, + {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cfb5c62ce023dfc410d6059c936dcf96442ba40814aefbfa575425a3a7f19dc"}, + {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68a32389b06b82c2fdd68276148d7b9275b5f5cf13e5417e4252f6d1a34f72a2"}, + {file = "contourpy-1.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94e848a6b83da10898cbf1311a815f770acc9b6a3f2d646f330d57eb4e87592e"}, + {file = "contourpy-1.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d78ab28a03c854a873787a0a42254a0ccb3cb133c672f645c9f9c8f3ae9d0800"}, + {file = "contourpy-1.3.0-cp39-cp39-win32.whl", hash = "sha256:81cb5ed4952aae6014bc9d0421dec7c5835c9c8c31cdf51910b708f548cf58e5"}, + {file = "contourpy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:14e262f67bd7e6eb6880bc564dcda30b15e351a594657e55b7eec94b6ef72843"}, + {file = "contourpy-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe41b41505a5a33aeaed2a613dccaeaa74e0e3ead6dd6fd3a118fb471644fd6c"}, + {file = "contourpy-1.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca7e17a65f72a5133bdbec9ecf22401c62bcf4821361ef7811faee695799779"}, + {file = "contourpy-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ec4dc6bf570f5b22ed0d7efba0dfa9c5b9e0431aeea7581aa217542d9e809a4"}, + {file = "contourpy-1.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:00ccd0dbaad6d804ab259820fa7cb0b8036bda0686ef844d24125d8287178ce0"}, + {file = "contourpy-1.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca947601224119117f7c19c9cdf6b3ab54c5726ef1d906aa4a69dfb6dd58102"}, + {file = "contourpy-1.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6ec93afeb848a0845a18989da3beca3eec2c0f852322efe21af1931147d12cb"}, + {file = "contourpy-1.3.0.tar.gz", hash = "sha256:7ffa0db17717a8ffb127efd0c95a4362d996b892c2904db72428d5b52e1938a4"}, +] + +[package.dependencies] +numpy = ">=1.23" + +[package.extras] +bokeh = ["bokeh", "selenium"] +docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] +mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.11.1)", "types-Pillow"] +test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] +test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist", "wurlitzer"] + [[package]] name = "cosmpy" version = "0.9.2" @@ -796,83 +1067,73 @@ requests = "*" [[package]] name = "coverage" -version = "7.6.1" +version = "7.6.4" description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, - {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, - {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, - {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, - {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, - {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, - {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, - {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, - {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, - {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, - {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, - {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, - {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, - {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, - {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, - {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, - {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, - {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, + {file = "coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07"}, + {file = "coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a"}, + {file = "coverage-7.6.4-cp310-cp310-win32.whl", hash = "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa"}, + {file = "coverage-7.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172"}, + {file = "coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b"}, + {file = "coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522"}, + {file = "coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf"}, + {file = "coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19"}, + {file = "coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2"}, + {file = "coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5"}, + {file = "coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17"}, + {file = "coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08"}, + {file = "coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9"}, + {file = "coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a"}, + {file = "coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e"}, + {file = "coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963"}, + {file = "coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f"}, + {file = "coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef"}, + {file = "coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e"}, + {file = "coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1"}, + {file = "coverage-7.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cb7fa111d21a6b55cbf633039f7bc2749e74932e3aa7cb7333f675a58a58bf3"}, + {file = "coverage-7.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11a223a14e91a4693d2d0755c7a043db43d96a7450b4f356d506c2562c48642c"}, + {file = "coverage-7.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a413a096c4cbac202433c850ee43fa326d2e871b24554da8327b01632673a076"}, + {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00a1d69c112ff5149cabe60d2e2ee948752c975d95f1e1096742e6077affd376"}, + {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f76846299ba5c54d12c91d776d9605ae33f8ae2b9d1d3c3703cf2db1a67f2c0"}, + {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fe439416eb6380de434886b00c859304338f8b19f6f54811984f3420a2e03858"}, + {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0294ca37f1ba500667b1aef631e48d875ced93ad5e06fa665a3295bdd1d95111"}, + {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6f01ba56b1c0e9d149f9ac85a2f999724895229eb36bd997b61e62999e9b0901"}, + {file = "coverage-7.6.4-cp39-cp39-win32.whl", hash = "sha256:bc66f0bf1d7730a17430a50163bb264ba9ded56739112368ba985ddaa9c3bd09"}, + {file = "coverage-7.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:c481b47f6b5845064c65a7bc78bc0860e635a9b055af0df46fdf1c58cebf8e8f"}, + {file = "coverage-7.6.4-pp39.pp310-none-any.whl", hash = "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e"}, + {file = "coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73"}, ] [package.dependencies] @@ -883,38 +1144,38 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "43.0.1" +version = "43.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"}, - {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"}, - {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"}, - {file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"}, - {file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"}, - {file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"}, - {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"}, - {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"}, - {file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"}, - {file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2"}, - {file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"}, + {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, + {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, + {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, + {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, + {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, + {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, + {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, ] [package.dependencies] @@ -927,120 +1188,107 @@ nox = ["nox"] pep8test = ["check-sdist", "click", "mypy", "ruff"] sdist = ["build"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "cryptography-vectors (==43.0.1)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] +[[package]] +name = "cycler" +version = "0.12.1" +description = "Composable style cycles" +optional = false +python-versions = ">=3.8" +files = [ + {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, + {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, +] + +[package.extras] +docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] +tests = ["pytest", "pytest-cov", "pytest-xdist"] + +[[package]] +name = "cython" +version = "3.0.11" +description = "The Cython compiler for writing C extensions in the Python language." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +files = [ + {file = "Cython-3.0.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:44292aae17524abb4b70a25111fe7dec1a0ad718711d47e3786a211d5408fdaa"}, + {file = "Cython-3.0.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75d45fbc20651c1b72e4111149fed3b33d270b0a4fb78328c54d965f28d55e1"}, + {file = "Cython-3.0.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d89a82937ce4037f092e9848a7bbcc65bc8e9fc9aef2bb74f5c15e7d21a73080"}, + {file = "Cython-3.0.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a8ea2e7e2d3bc0d8630dafe6c4a5a89485598ff8a61885b74f8ed882597efd5"}, + {file = "Cython-3.0.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cee29846471ce60226b18e931d8c1c66a158db94853e3e79bc2da9bd22345008"}, + {file = "Cython-3.0.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eeb6860b0f4bfa402de8929833fe5370fa34069c7ebacb2d543cb017f21fb891"}, + {file = "Cython-3.0.11-cp310-cp310-win32.whl", hash = "sha256:3699391125ab344d8d25438074d1097d9ba0fb674d0320599316cfe7cf5f002a"}, + {file = "Cython-3.0.11-cp310-cp310-win_amd64.whl", hash = "sha256:d02f4ebe15aac7cdacce1a628e556c1983f26d140fd2e0ac5e0a090e605a2d38"}, + {file = "Cython-3.0.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75ba1c70b6deeaffbac123856b8d35f253da13552207aa969078611c197377e4"}, + {file = "Cython-3.0.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af91497dc098718e634d6ec8f91b182aea6bb3690f333fc9a7777bc70abe8810"}, + {file = "Cython-3.0.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3999fb52d3328a6a5e8c63122b0a8bd110dfcdb98dda585a3def1426b991cba7"}, + {file = "Cython-3.0.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d566a4e09b8979be8ab9f843bac0dd216c81f5e5f45661a9b25cd162ed80508c"}, + {file = "Cython-3.0.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:46aec30f217bdf096175a1a639203d44ac73a36fe7fa3dd06bd012e8f39eca0f"}, + {file = "Cython-3.0.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ddd1fe25af330f4e003421636746a546474e4ccd8f239f55d2898d80983d20ed"}, + {file = "Cython-3.0.11-cp311-cp311-win32.whl", hash = "sha256:221de0b48bf387f209003508e602ce839a80463522fc6f583ad3c8d5c890d2c1"}, + {file = "Cython-3.0.11-cp311-cp311-win_amd64.whl", hash = "sha256:3ff8ac1f0ecd4f505db4ab051e58e4531f5d098b6ac03b91c3b902e8d10c67b3"}, + {file = "Cython-3.0.11-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:11996c40c32abf843ba652a6d53cb15944c88d91f91fc4e6f0028f5df8a8f8a1"}, + {file = "Cython-3.0.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63f2c892e9f9c1698ecfee78205541623eb31cd3a1b682668be7ac12de94aa8e"}, + {file = "Cython-3.0.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b14c24f1dc4c4c9d997cca8d1b7fb01187a218aab932328247dcf5694a10102"}, + {file = "Cython-3.0.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8eed5c015685106db15dd103fd040948ddca9197b1dd02222711815ea782a27"}, + {file = "Cython-3.0.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780f89c95b8aec1e403005b3bf2f0a2afa060b3eba168c86830f079339adad89"}, + {file = "Cython-3.0.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a690f2ff460682ea985e8d38ec541be97e0977fa0544aadc21efc116ff8d7579"}, + {file = "Cython-3.0.11-cp312-cp312-win32.whl", hash = "sha256:2252b5aa57621848e310fe7fa6f7dce5f73aa452884a183d201a8bcebfa05a00"}, + {file = "Cython-3.0.11-cp312-cp312-win_amd64.whl", hash = "sha256:da394654c6da15c1d37f0b7ec5afd325c69a15ceafee2afba14b67a5df8a82c8"}, + {file = "Cython-3.0.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4341d6a64d47112884e0bcf31e6c075268220ee4cd02223047182d4dda94d637"}, + {file = "Cython-3.0.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:351955559b37e6c98b48aecb178894c311be9d731b297782f2b78d111f0c9015"}, + {file = "Cython-3.0.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c02361af9bfa10ff1ccf967fc75159e56b1c8093caf565739ed77a559c1f29f"}, + {file = "Cython-3.0.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6823aef13669a32caf18bbb036de56065c485d9f558551a9b55061acf9c4c27f"}, + {file = "Cython-3.0.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6fb68cef33684f8cc97987bee6ae919eee7e18ee6a3ad7ed9516b8386ef95ae6"}, + {file = "Cython-3.0.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:790263b74432cb997740d73665f4d8d00b9cd1cecbdd981d93591ddf993d4f12"}, + {file = "Cython-3.0.11-cp313-cp313-win32.whl", hash = "sha256:e6dd395d1a704e34a9fac00b25f0036dce6654c6b898be6f872ac2bb4f2eda48"}, + {file = "Cython-3.0.11-cp313-cp313-win_amd64.whl", hash = "sha256:52186101d51497519e99b60d955fd5cb3bf747c67f00d742e70ab913f1e42d31"}, + {file = "Cython-3.0.11-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c69d5cad51388522b98a99b4be1b77316de85b0c0523fa865e0ea58bbb622e0a"}, + {file = "Cython-3.0.11-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8acdc87e9009110adbceb7569765eb0980129055cc954c62f99fe9f094c9505e"}, + {file = "Cython-3.0.11-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1dd47865f4c0a224da73acf83d113f93488d17624e2457dce1753acdfb1cc40c"}, + {file = "Cython-3.0.11-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:301bde949b4f312a1c70e214b0c3bc51a3f955d466010d2f68eb042df36447b0"}, + {file = "Cython-3.0.11-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:f3953d2f504176f929862e5579cfc421860c33e9707f585d70d24e1096accdf7"}, + {file = "Cython-3.0.11-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:3f2b062f6df67e8a56c75e500ca330cf62c85ac26dd7fd006f07ef0f83aebfa3"}, + {file = "Cython-3.0.11-cp36-cp36m-win32.whl", hash = "sha256:c3d68751668c66c7a140b6023dba5d5d507f72063407bb609d3a5b0f3b8dfbe4"}, + {file = "Cython-3.0.11-cp36-cp36m-win_amd64.whl", hash = "sha256:bcd29945fafd12484cf37b1d84f12f0e7a33ba3eac5836531c6bd5283a6b3a0c"}, + {file = "Cython-3.0.11-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4e9a8d92978b15a0c7ca7f98447c6c578dc8923a0941d9d172d0b077cb69c576"}, + {file = "Cython-3.0.11-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:421017466e9260aca86823974e26e158e6358622f27c0f4da9c682f3b6d2e624"}, + {file = "Cython-3.0.11-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d80a7232938d523c1a12f6b1794ab5efb1ae77ad3fde79de4bb558d8ab261619"}, + {file = "Cython-3.0.11-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfa550d9ae39e827a6e7198076df763571cb53397084974a6948af558355e028"}, + {file = "Cython-3.0.11-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:aedceb6090a60854b31bf9571dc55f642a3fa5b91f11b62bcef167c52cac93d8"}, + {file = "Cython-3.0.11-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:473d35681d9f93ce380e6a7c8feb2d65fc6333bd7117fbc62989e404e241dbb0"}, + {file = "Cython-3.0.11-cp37-cp37m-win32.whl", hash = "sha256:3379c6521e25aa6cd7703bb7d635eaca75c0f9c7f1b0fdd6dd15a03bfac5f68d"}, + {file = "Cython-3.0.11-cp37-cp37m-win_amd64.whl", hash = "sha256:14701edb3107a5d9305a82d9d646c4f28bfecbba74b26cc1ee2f4be08f602057"}, + {file = "Cython-3.0.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:598699165cfa7c6d69513ee1bffc9e1fdd63b00b624409174c388538aa217975"}, + {file = "Cython-3.0.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0583076c4152b417a3a8a5d81ec02f58c09b67d3f22d5857e64c8734ceada8c"}, + {file = "Cython-3.0.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52205347e916dd65d2400b977df4c697390c3aae0e96275a438cc4ae85dadc08"}, + {file = "Cython-3.0.11-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:989899a85f0d9a57cebb508bd1f194cb52f0e3f7e22ac259f33d148d6422375c"}, + {file = "Cython-3.0.11-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:53b6072a89049a991d07f42060f65398448365c59c9cb515c5925b9bdc9d71f8"}, + {file = "Cython-3.0.11-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f988f7f8164a6079c705c39e2d75dbe9967e3dacafe041420d9af7b9ee424162"}, + {file = "Cython-3.0.11-cp38-cp38-win32.whl", hash = "sha256:a1f4cbc70f6b7f0c939522118820e708e0d490edca42d852fa8004ec16780be2"}, + {file = "Cython-3.0.11-cp38-cp38-win_amd64.whl", hash = "sha256:187685e25e037320cae513b8cc4bf9dbc4465c037051aede509cbbf207524de2"}, + {file = "Cython-3.0.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0fc6fdd6fa493be7bdda22355689d5446ac944cd71286f6f44a14b0d67ee3ff5"}, + {file = "Cython-3.0.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b1d1f6f94cc5d42a4591f6d60d616786b9cd15576b112bc92a23131fcf38020"}, + {file = "Cython-3.0.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4ab2b92a3e6ed552adbe9350fd2ef3aa0cc7853cf91569f9dbed0c0699bbeab"}, + {file = "Cython-3.0.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:104d6f2f2c827ccc5e9e42c80ef6773a6aa94752fe6bc5b24a4eab4306fb7f07"}, + {file = "Cython-3.0.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:13062ce556a1e98d2821f7a0253b50569fdc98c36efd6653a65b21e3f8bbbf5f"}, + {file = "Cython-3.0.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:525d09b3405534763fa73bd78c8e51ac8264036ce4c16d37dfd1555a7da6d3a7"}, + {file = "Cython-3.0.11-cp39-cp39-win32.whl", hash = "sha256:b8c7e514075696ca0f60c337f9e416e61d7ccbc1aa879a56c39181ed90ec3059"}, + {file = "Cython-3.0.11-cp39-cp39-win_amd64.whl", hash = "sha256:8948802e1f5677a673ea5d22a1e7e273ca5f83e7a452786ca286eebf97cee67c"}, + {file = "Cython-3.0.11-py2.py3-none-any.whl", hash = "sha256:0e25f6425ad4a700d7f77cd468da9161e63658837d1bc34861a9861a4ef6346d"}, + {file = "cython-3.0.11.tar.gz", hash = "sha256:7146dd2af8682b4ca61331851e6aebce9fe5158e75300343f80c07ca80b1faff"}, +] + [[package]] name = "cytoolz" -version = "0.12.3" +version = "0.11.2" description = "Cython implementation of Toolz: High performance functional utilities" optional = false -python-versions = ">=3.7" +python-versions = ">=3.5" files = [ - {file = "cytoolz-0.12.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bbe58e26c84b163beba0fbeacf6b065feabc8f75c6d3fe305550d33f24a2d346"}, - {file = "cytoolz-0.12.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c51b66ada9bfdb88cf711bf350fcc46f82b83a4683cf2413e633c31a64df6201"}, - {file = "cytoolz-0.12.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e70d9c615e5c9dc10d279d1e32e846085fe1fd6f08d623ddd059a92861f4e3dd"}, - {file = "cytoolz-0.12.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a83f4532707963ae1a5108e51fdfe1278cc8724e3301fee48b9e73e1316de64f"}, - {file = "cytoolz-0.12.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d028044524ee2e815f36210a793c414551b689d4f4eda28f8bbb0883ad78bf5f"}, - {file = "cytoolz-0.12.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c2875bcd1397d0627a09a4f9172fa513185ad302c63758efc15b8eb33cc2a98"}, - {file = "cytoolz-0.12.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:131ff4820e5d64a25d7ad3c3556f2d8aa65c66b3f021b03f8a8e98e4180dd808"}, - {file = "cytoolz-0.12.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:04afa90d9d9d18394c40d9bed48c51433d08b57c042e0e50c8c0f9799735dcbd"}, - {file = "cytoolz-0.12.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:dc1ca9c610425f9854323669a671fc163300b873731584e258975adf50931164"}, - {file = "cytoolz-0.12.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:bfa3f8e01bc423a933f2e1c510cbb0632c6787865b5242857cc955cae220d1bf"}, - {file = "cytoolz-0.12.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:f702e295dddef5f8af4a456db93f114539b8dc2a7a9bc4de7c7e41d169aa6ec3"}, - {file = "cytoolz-0.12.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0fbad1fb9bb47e827d00e01992a099b0ba79facf5e5aa453be066033232ac4b5"}, - {file = "cytoolz-0.12.3-cp310-cp310-win32.whl", hash = "sha256:8587c3c3dbe78af90c5025288766ac10dc2240c1e76eb0a93a4e244c265ccefd"}, - {file = "cytoolz-0.12.3-cp310-cp310-win_amd64.whl", hash = "sha256:9e45803d9e75ef90a2f859ef8f7f77614730f4a8ce1b9244375734567299d239"}, - {file = "cytoolz-0.12.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3ac4f2fb38bbc67ff1875b7d2f0f162a247f43bd28eb7c9d15e6175a982e558d"}, - {file = "cytoolz-0.12.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0cf1e1e96dd86829a0539baf514a9c8473a58fbb415f92401a68e8e52a34ecd5"}, - {file = "cytoolz-0.12.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08a438701c6141dd34eaf92e9e9a1f66e23a22f7840ef8a371eba274477de85d"}, - {file = "cytoolz-0.12.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6b6f11b0d7ed91be53166aeef2a23a799e636625675bb30818f47f41ad31821"}, - {file = "cytoolz-0.12.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7fde09384d23048a7b4ac889063761e44b89a0b64015393e2d1d21d5c1f534a"}, - {file = "cytoolz-0.12.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d3bfe45173cc8e6c76206be3a916d8bfd2214fb2965563e288088012f1dabfc"}, - {file = "cytoolz-0.12.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27513a5d5b6624372d63313574381d3217a66e7a2626b056c695179623a5cb1a"}, - {file = "cytoolz-0.12.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d294e5e81ff094fe920fd545052ff30838ea49f9e91227a55ecd9f3ca19774a0"}, - {file = "cytoolz-0.12.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:727b01a2004ddb513496507a695e19b5c0cfebcdfcc68349d3efd92a1c297bf4"}, - {file = "cytoolz-0.12.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:fe1e1779a39dbe83f13886d2b4b02f8c4b10755e3c8d9a89b630395f49f4f406"}, - {file = "cytoolz-0.12.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:de74ef266e2679c3bf8b5fc20cee4fc0271ba13ae0d9097b1491c7a9bcadb389"}, - {file = "cytoolz-0.12.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e04d22049233394e0b08193aca9737200b4a2afa28659d957327aa780ddddf2"}, - {file = "cytoolz-0.12.3-cp311-cp311-win32.whl", hash = "sha256:20d36430d8ac809186736fda735ee7d595b6242bdb35f69b598ef809ebfa5605"}, - {file = "cytoolz-0.12.3-cp311-cp311-win_amd64.whl", hash = "sha256:780c06110f383344d537f48d9010d79fa4f75070d214fc47f389357dd4f010b6"}, - {file = "cytoolz-0.12.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:86923d823bd19ce35805953b018d436f6b862edd6a7c8b747a13d52b39ed5716"}, - {file = "cytoolz-0.12.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3e61acfd029bfb81c2c596249b508dfd2b4f72e31b7b53b62e5fb0507dd7293"}, - {file = "cytoolz-0.12.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd728f4e6051af6af234651df49319da1d813f47894d4c3c8ab7455e01703a37"}, - {file = "cytoolz-0.12.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe8c6267caa7ec67bcc37e360f0d8a26bc3bdce510b15b97f2f2e0143bdd3673"}, - {file = "cytoolz-0.12.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99462abd8323c52204a2a0ce62454ce8fa0f4e94b9af397945c12830de73f27e"}, - {file = "cytoolz-0.12.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da125221b1fa25c690fcd030a54344cecec80074df018d906fc6a99f46c1e3a6"}, - {file = "cytoolz-0.12.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c18e351956f70db9e2d04ff02f28e9a41839250d3f936a4c8a1eabd1c3094d2"}, - {file = "cytoolz-0.12.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:921e6d2440ac758c4945c587b1d1d9b781b72737ac0c0ca5d5e02ca1db8bded2"}, - {file = "cytoolz-0.12.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1651a9bd591a8326329ce1d6336f3129161a36d7061a4d5ea9e5377e033364cf"}, - {file = "cytoolz-0.12.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8893223b87c2782bd59f9c4bd5c7bf733edd8728b523c93efb91d7468b486528"}, - {file = "cytoolz-0.12.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:e4d2961644153c5ae186db964aa9f6109da81b12df0f1d3494b4e5cf2c332ee2"}, - {file = "cytoolz-0.12.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:71b6eb97f6695f7ba8ce69c49b707a351c5f46fd97f5aeb5f6f2fb0d6e72b887"}, - {file = "cytoolz-0.12.3-cp312-cp312-win32.whl", hash = "sha256:cee3de65584e915053412cd178729ff510ad5f8f585c21c5890e91028283518f"}, - {file = "cytoolz-0.12.3-cp312-cp312-win_amd64.whl", hash = "sha256:9eef0d23035fa4dcfa21e570961e86c375153a7ee605cdd11a8b088c24f707f6"}, - {file = "cytoolz-0.12.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9a38332cfad2a91e89405b7c18b3f00e2edc951c225accbc217597d3e4e9fde"}, - {file = "cytoolz-0.12.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f501ae1353071fa5d6677437bbeb1aeb5622067dce0977cedc2c5ec5843b202"}, - {file = "cytoolz-0.12.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:56f899758146a52e2f8cfb3fb6f4ca19c1e5814178c3d584de35f9e4d7166d91"}, - {file = "cytoolz-0.12.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:800f0526adf9e53d3c6acda748f4def1f048adaa780752f154da5cf22aa488a2"}, - {file = "cytoolz-0.12.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0976a3fcb81d065473173e9005848218ce03ddb2ec7d40dd6a8d2dba7f1c3ae"}, - {file = "cytoolz-0.12.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c835eab01466cb67d0ce6290601ebef2d82d8d0d0a285ed0d6e46989e4a7a71a"}, - {file = "cytoolz-0.12.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4fba0616fcd487e34b8beec1ad9911d192c62e758baa12fcb44448b9b6feae22"}, - {file = "cytoolz-0.12.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6f6e8207d732651e0204779e1ba5a4925c93081834570411f959b80681f8d333"}, - {file = "cytoolz-0.12.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:8119bf5961091cfe644784d0bae214e273b3b3a479f93ee3baab97bbd995ccfe"}, - {file = "cytoolz-0.12.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:7ad1331cb68afeec58469c31d944a2100cee14eac221553f0d5218ace1a0b25d"}, - {file = "cytoolz-0.12.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:92c53d508fb8a4463acc85b322fa24734efdc66933a5c8661bdc862103a3373d"}, - {file = "cytoolz-0.12.3-cp37-cp37m-win32.whl", hash = "sha256:2c6dd75dae3d84fa8988861ab8b1189d2488cb8a9b8653828f9cd6126b5e7abd"}, - {file = "cytoolz-0.12.3-cp37-cp37m-win_amd64.whl", hash = "sha256:caf07a97b5220e6334dd32c8b6d8b2bd255ca694eca5dfe914bb5b880ee66cdb"}, - {file = "cytoolz-0.12.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ed0cfb9326747759e2ad81cb6e45f20086a273b67ac3a4c00b19efcbab007c60"}, - {file = "cytoolz-0.12.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:96a5a0292575c3697121f97cc605baf2fd125120c7dcdf39edd1a135798482ca"}, - {file = "cytoolz-0.12.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b76f2f50a789c44d6fd7f773ec43d2a8686781cd52236da03f7f7d7998989bee"}, - {file = "cytoolz-0.12.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2905fdccacc64b4beba37f95cab9d792289c80f4d70830b70de2fc66c007ec01"}, - {file = "cytoolz-0.12.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ebe23028eac51251f22ba01dba6587d30aa9c320372ca0c14eeab67118ec3f"}, - {file = "cytoolz-0.12.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96c715404a3825e37fe3966fe84c5f8a1f036e7640b2a02dbed96cac0c933451"}, - {file = "cytoolz-0.12.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bac0adffc1b6b6a4c5f1fd1dd2161afb720bcc771a91016dc6bdba59af0a5d3"}, - {file = "cytoolz-0.12.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:37441bf4a2a4e2e0fe9c3b0ea5e72db352f5cca03903977ffc42f6f6c5467be9"}, - {file = "cytoolz-0.12.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f04037302049cb30033f7fa4e1d0e44afe35ed6bfcf9b380fc11f2a27d3ed697"}, - {file = "cytoolz-0.12.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f37b60e66378e7a116931d7220f5352186abfcc950d64856038aa2c01944929c"}, - {file = "cytoolz-0.12.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ec9be3e4b6f86ea8b294d34c990c99d2ba6c526ef1e8f46f1d52c263d4f32cd7"}, - {file = "cytoolz-0.12.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0e9199c9e3fbf380a92b8042c677eb9e7ed4bccb126de5e9c0d26f5888d96788"}, - {file = "cytoolz-0.12.3-cp38-cp38-win32.whl", hash = "sha256:18cd61e078bd6bffe088e40f1ed02001387c29174750abce79499d26fa57f5eb"}, - {file = "cytoolz-0.12.3-cp38-cp38-win_amd64.whl", hash = "sha256:765b8381d4003ceb1a07896a854eee2c31ebc950a4ae17d1e7a17c2a8feb2a68"}, - {file = "cytoolz-0.12.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b4a52dd2a36b0a91f7aa50ca6c8509057acc481a24255f6cb07b15d339a34e0f"}, - {file = "cytoolz-0.12.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:581f1ce479769fe7eeb9ae6d87eadb230df8c7c5fff32138162cdd99d7fb8fc3"}, - {file = "cytoolz-0.12.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46f505d4c6eb79585c8ad0b9dc140ef30a138c880e4e3b40230d642690e36366"}, - {file = "cytoolz-0.12.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59276021619b432a5c21c01cda8320b9cc7dbc40351ffc478b440bfccd5bbdd3"}, - {file = "cytoolz-0.12.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e44f4c25e1e7cf6149b499c74945a14649c8866d36371a2c2d2164e4649e7755"}, - {file = "cytoolz-0.12.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c64f8e60c1dd69e4d5e615481f2d57937746f4a6be2d0f86e9e7e3b9e2243b5e"}, - {file = "cytoolz-0.12.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33c63186f3bf9d7ef1347bc0537bb9a0b4111a0d7d6e619623cabc18fef0dc3b"}, - {file = "cytoolz-0.12.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fdddb9d988405f24035234f1e8d1653ab2e48cc2404226d21b49a129aefd1d25"}, - {file = "cytoolz-0.12.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6986632d8a969ea1e720990c818dace1a24c11015fd7c59b9fea0b65ef71f726"}, - {file = "cytoolz-0.12.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0ba1cbc4d9cd7571c917f88f4a069568e5121646eb5d82b2393b2cf84712cf2a"}, - {file = "cytoolz-0.12.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7d267ffc9a36c0a9a58c7e0adc9fa82620f22e4a72533e15dd1361f57fc9accf"}, - {file = "cytoolz-0.12.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:95e878868a172a41fbf6c505a4b967309e6870e22adc7b1c3b19653d062711fa"}, - {file = "cytoolz-0.12.3-cp39-cp39-win32.whl", hash = "sha256:8e21932d6d260996f7109f2a40b2586070cb0a0cf1d65781e156326d5ebcc329"}, - {file = "cytoolz-0.12.3-cp39-cp39-win_amd64.whl", hash = "sha256:0d8edfbc694af6c9bda4db56643fb8ed3d14e47bec358c2f1417de9a12d6d1fb"}, - {file = "cytoolz-0.12.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:55f9bd1ae6c2a27eda5abe2a0b65a83029d2385c5a1da7b8ef47af5905d7e905"}, - {file = "cytoolz-0.12.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2d271393c378282727f1231d40391ae93b93ddc0997448acc21dd0cb6a1e56d"}, - {file = "cytoolz-0.12.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee98968d6a66ee83a8ceabf31182189ab5d8598998c8ce69b6d5843daeb2db60"}, - {file = "cytoolz-0.12.3-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01cfb8518828c1189200c02a5010ea404407fb18fd5589e29c126e84bbeadd36"}, - {file = "cytoolz-0.12.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:456395d7aec01db32bf9e6db191d667347c78d8d48e77234521fa1078f60dabb"}, - {file = "cytoolz-0.12.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cd88028bb897fba99ddd84f253ca6bef73ecb7bdf3f3cf25bc493f8f97d3c7c5"}, - {file = "cytoolz-0.12.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59b19223e7f7bd7a73ec3aa6fdfb73b579ff09c2bc0b7d26857eec2d01a58c76"}, - {file = "cytoolz-0.12.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a79d72b08048a0980a59457c239555f111ac0c8bdc140c91a025f124104dbb4"}, - {file = "cytoolz-0.12.3-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1dd70141b32b717696a72b8876e86bc9c6f8eff995c1808e299db3541213ff82"}, - {file = "cytoolz-0.12.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:a1445c91009eb775d479e88954c51d0b4cf9a1e8ce3c503c2672d17252882647"}, - {file = "cytoolz-0.12.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ca6a9a9300d5bda417d9090107c6d2b007683efc59d63cc09aca0e7930a08a85"}, - {file = "cytoolz-0.12.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be6feb903d2a08a4ba2e70e950e862fd3be9be9a588b7c38cee4728150a52918"}, - {file = "cytoolz-0.12.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b6f43f086e5a965d33d62a145ae121b4ccb6e0789ac0acc895ce084fec8c65"}, - {file = "cytoolz-0.12.3-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:534fa66db8564d9b13872d81d54b6b09ae592c585eb826aac235bd6f1830f8ad"}, - {file = "cytoolz-0.12.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:fea649f979def23150680de1bd1d09682da3b54932800a0f90f29fc2a6c98ba8"}, - {file = "cytoolz-0.12.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a447247ed312dd64e3a8d9483841ecc5338ee26d6e6fbd29cd373ed030db0240"}, - {file = "cytoolz-0.12.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba3f843aa89f35467b38c398ae5b980a824fdbdb94065adc6ec7c47a0a22f4c7"}, - {file = "cytoolz-0.12.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:582c22f97a380211fb36a7b65b1beeb84ea11d82015fa84b054be78580390082"}, - {file = "cytoolz-0.12.3-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47feb089506fc66e1593cd9ade3945693a9d089a445fbe9a11385cab200b9f22"}, - {file = "cytoolz-0.12.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ba9002d2f043943744a9dc8e50a47362bcb6e6f360dc0a1abcb19642584d87bb"}, - {file = "cytoolz-0.12.3.tar.gz", hash = "sha256:4503dc59f4ced53a54643272c61dc305d1dbbfbd7d6bdf296948de9f34c3a282"}, + {file = "cytoolz-0.11.2.tar.gz", hash = "sha256:ea23663153806edddce7e4153d1d407d62357c05120a4e8485bddf1bd5ab22b4"}, ] [package.dependencies] @@ -1049,15 +1297,37 @@ toolz = ">=0.8.0" [package.extras] cython = ["cython"] +[[package]] +name = "dateparser" +version = "1.2.0" +description = "Date parsing library designed to parse dates from HTML pages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "dateparser-1.2.0-py2.py3-none-any.whl", hash = "sha256:0b21ad96534e562920a0083e97fd45fa959882d4162acc358705144520a35830"}, + {file = "dateparser-1.2.0.tar.gz", hash = "sha256:7975b43a4222283e0ae15be7b4999d08c9a70e2d378ac87385b1ccf2cffbbb30"}, +] + +[package.dependencies] +python-dateutil = "*" +pytz = "*" +regex = "<2019.02.19 || >2019.02.19,<2021.8.27 || >2021.8.27" +tzlocal = "*" + +[package.extras] +calendars = ["convertdate", "hijri-converter"] +fasttext = ["fasttext"] +langdetect = ["langdetect"] + [[package]] name = "distlib" -version = "0.3.8" +version = "0.3.9" description = "Distribution utilities" optional = false python-versions = "*" files = [ - {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, - {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, ] [[package]] @@ -1136,24 +1406,25 @@ gmpy2 = ["gmpy2"] [[package]] name = "eth-abi" -version = "5.1.0" +version = "4.0.0" description = "eth_abi: Python utilities for working with Ethereum ABI definitions, especially encoding and decoding" optional = false -python-versions = "<4,>=3.8" +python-versions = ">=3.7, <4" files = [ - {file = "eth_abi-5.1.0-py3-none-any.whl", hash = "sha256:84cac2626a7db8b7d9ebe62b0fdca676ab1014cc7f777189e3c0cd721a4c16d8"}, - {file = "eth_abi-5.1.0.tar.gz", hash = "sha256:33ddd756206e90f7ddff1330cc8cac4aa411a824fe779314a0a52abea2c8fc14"}, + {file = "eth_abi-4.0.0-py3-none-any.whl", hash = "sha256:79d258669f3505319e53638d644a75e1c816db552a1ab1927c3063763cc41031"}, + {file = "eth_abi-4.0.0.tar.gz", hash = "sha256:6949baba61a2c453f0719309ca145e8876a1cbae7ba377c991e67240c13ec7fc"}, ] [package.dependencies] eth-typing = ">=3.0.0" eth-utils = ">=2.0.0" -parsimonious = ">=0.10.0,<0.11.0" +parsimonious = ">=0.9.0,<0.10.0" [package.extras] -dev = ["build (>=0.9.0)", "bumpversion (>=0.5.3)", "eth-hash[pycryptodome]", "hypothesis (>=4.18.2,<5.0.0)", "ipython", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-pythonpath (>=0.7.1)", "pytest-timeout (>=2.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["sphinx (>=6.0.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)"] -test = ["eth-hash[pycryptodome]", "hypothesis (>=4.18.2,<5.0.0)", "pytest (>=7.0.0)", "pytest-pythonpath (>=0.7.1)", "pytest-timeout (>=2.0.0)", "pytest-xdist (>=2.4.0)"] +dev = ["black", "bumpversion (>=0.5.3,<1)", "eth-hash[pycryptodome]", "flake8", "hypothesis (>=4.18.2,<5.0.0)", "ipython", "isort (>=4.2.15,<5)", "jinja2 (>=3.0.0,<3.1.0)", "mypy (==0.910)", "pydocstyle (>=6.0.0,<7)", "pytest (>=6.2.5,<7)", "pytest-pythonpath (>=0.7.1)", "pytest-watch (>=4.1.0,<5)", "pytest-xdist (>=2.5.0,<3)", "sphinx (>=4.5.0,<5)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (==18.5.0)", "tox (>=2.9.1,<3)", "twine", "wheel"] +doc = ["jinja2 (>=3.0.0,<3.1.0)", "sphinx (>=4.5.0,<5)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (==18.5.0)"] +lint = ["black", "flake8", "isort (>=4.2.15,<5)", "mypy (==0.910)", "pydocstyle (>=6.0.0,<7)"] +test = ["eth-hash[pycryptodome]", "hypothesis (>=4.18.2,<5.0.0)", "pytest (>=6.2.5,<7)", "pytest-pythonpath (>=0.7.1)", "pytest-xdist (>=2.5.0,<3)", "tox (>=2.9.1,<3)"] tools = ["hypothesis (>=4.18.2,<5.0.0)"] [[package]] @@ -1292,13 +1563,13 @@ test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] [[package]] name = "eth-utils" -version = "2.3.1" +version = "2.2.0" description = "eth-utils: Common utility functions for python code that interacts with Ethereum" optional = false python-versions = ">=3.7,<4" files = [ - {file = "eth-utils-2.3.1.tar.gz", hash = "sha256:56a969b0536d4969dcb27e580521de35abf2dbed8b1bf072b5c80770c4324e27"}, - {file = "eth_utils-2.3.1-py3-none-any.whl", hash = "sha256:614eedc5ffcaf4e6708ca39e23b12bd69526a312068c1170c773bd1307d13972"}, + {file = "eth-utils-2.2.0.tar.gz", hash = "sha256:7f1a9e10400ee332432a778c321f446abaedb8f538df550e7c9964f446f7e265"}, + {file = "eth_utils-2.2.0-py3-none-any.whl", hash = "sha256:d6e107d522f83adff31237a95bdcc329ac0819a3ac698fe43c8a56fd80813eab"}, ] [package.dependencies] @@ -1309,7 +1580,7 @@ toolz = {version = ">0.8.2", markers = "implementation_name == \"pypy\""} [package.extras] dev = ["black (>=23)", "build (>=0.9.0)", "bumpversion (>=0.5.3)", "eth-hash[pycryptodome]", "flake8 (==3.8.3)", "hypothesis (>=4.43.0)", "ipython", "isort (>=5.11.0)", "mypy (==0.971)", "pydocstyle (>=5.0.0)", "pytest (>=7.0.0)", "pytest-watch (>=4.1.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=5.0.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)", "tox (>=4.0.0)", "twine", "types-setuptools", "wheel"] -docs = ["sphinx (>=5.0.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)"] +doc = ["sphinx (>=5.0.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)"] lint = ["black (>=23)", "flake8 (==3.8.3)", "isort (>=5.11.0)", "mypy (==0.971)", "pydocstyle (>=5.0.0)", "types-setuptools"] test = ["hypothesis (>=4.43.0)", "mypy (==0.971)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "types-setuptools"] @@ -1364,6 +1635,77 @@ Werkzeug = ">=2.0" async = ["asgiref (>=3.2)"] dotenv = ["python-dotenv"] +[[package]] +name = "fonttools" +version = "4.54.1" +description = "Tools to manipulate font files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fonttools-4.54.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ed7ee041ff7b34cc62f07545e55e1468808691dddfd315d51dd82a6b37ddef2"}, + {file = "fonttools-4.54.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41bb0b250c8132b2fcac148e2e9198e62ff06f3cc472065dff839327945c5882"}, + {file = "fonttools-4.54.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7965af9b67dd546e52afcf2e38641b5be956d68c425bef2158e95af11d229f10"}, + {file = "fonttools-4.54.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:278913a168f90d53378c20c23b80f4e599dca62fbffae4cc620c8eed476b723e"}, + {file = "fonttools-4.54.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0e88e3018ac809b9662615072dcd6b84dca4c2d991c6d66e1970a112503bba7e"}, + {file = "fonttools-4.54.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4aa4817f0031206e637d1e685251ac61be64d1adef111060df84fdcbc6ab6c44"}, + {file = "fonttools-4.54.1-cp310-cp310-win32.whl", hash = "sha256:7e3b7d44e18c085fd8c16dcc6f1ad6c61b71ff463636fcb13df7b1b818bd0c02"}, + {file = "fonttools-4.54.1-cp310-cp310-win_amd64.whl", hash = "sha256:dd9cc95b8d6e27d01e1e1f1fae8559ef3c02c76317da650a19047f249acd519d"}, + {file = "fonttools-4.54.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5419771b64248484299fa77689d4f3aeed643ea6630b2ea750eeab219588ba20"}, + {file = "fonttools-4.54.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:301540e89cf4ce89d462eb23a89464fef50915255ece765d10eee8b2bf9d75b2"}, + {file = "fonttools-4.54.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76ae5091547e74e7efecc3cbf8e75200bc92daaeb88e5433c5e3e95ea8ce5aa7"}, + {file = "fonttools-4.54.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82834962b3d7c5ca98cb56001c33cf20eb110ecf442725dc5fdf36d16ed1ab07"}, + {file = "fonttools-4.54.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d26732ae002cc3d2ecab04897bb02ae3f11f06dd7575d1df46acd2f7c012a8d8"}, + {file = "fonttools-4.54.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58974b4987b2a71ee08ade1e7f47f410c367cdfc5a94fabd599c88165f56213a"}, + {file = "fonttools-4.54.1-cp311-cp311-win32.whl", hash = "sha256:ab774fa225238986218a463f3fe151e04d8c25d7de09df7f0f5fce27b1243dbc"}, + {file = "fonttools-4.54.1-cp311-cp311-win_amd64.whl", hash = "sha256:07e005dc454eee1cc60105d6a29593459a06321c21897f769a281ff2d08939f6"}, + {file = "fonttools-4.54.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:54471032f7cb5fca694b5f1a0aaeba4af6e10ae989df408e0216f7fd6cdc405d"}, + {file = "fonttools-4.54.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fa92cb248e573daab8d032919623cc309c005086d743afb014c836636166f08"}, + {file = "fonttools-4.54.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a911591200114969befa7f2cb74ac148bce5a91df5645443371aba6d222e263"}, + {file = "fonttools-4.54.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93d458c8a6a354dc8b48fc78d66d2a8a90b941f7fec30e94c7ad9982b1fa6bab"}, + {file = "fonttools-4.54.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5eb2474a7c5be8a5331146758debb2669bf5635c021aee00fd7c353558fc659d"}, + {file = "fonttools-4.54.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9c563351ddc230725c4bdf7d9e1e92cbe6ae8553942bd1fb2b2ff0884e8b714"}, + {file = "fonttools-4.54.1-cp312-cp312-win32.whl", hash = "sha256:fdb062893fd6d47b527d39346e0c5578b7957dcea6d6a3b6794569370013d9ac"}, + {file = "fonttools-4.54.1-cp312-cp312-win_amd64.whl", hash = "sha256:e4564cf40cebcb53f3dc825e85910bf54835e8a8b6880d59e5159f0f325e637e"}, + {file = "fonttools-4.54.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6e37561751b017cf5c40fce0d90fd9e8274716de327ec4ffb0df957160be3bff"}, + {file = "fonttools-4.54.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:357cacb988a18aace66e5e55fe1247f2ee706e01debc4b1a20d77400354cddeb"}, + {file = "fonttools-4.54.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8e953cc0bddc2beaf3a3c3b5dd9ab7554677da72dfaf46951e193c9653e515a"}, + {file = "fonttools-4.54.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:58d29b9a294573d8319f16f2f79e42428ba9b6480442fa1836e4eb89c4d9d61c"}, + {file = "fonttools-4.54.1-cp313-cp313-win32.whl", hash = "sha256:9ef1b167e22709b46bf8168368b7b5d3efeaaa746c6d39661c1b4405b6352e58"}, + {file = "fonttools-4.54.1-cp313-cp313-win_amd64.whl", hash = "sha256:262705b1663f18c04250bd1242b0515d3bbae177bee7752be67c979b7d47f43d"}, + {file = "fonttools-4.54.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ed2f80ca07025551636c555dec2b755dd005e2ea8fbeb99fc5cdff319b70b23b"}, + {file = "fonttools-4.54.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9dc080e5a1c3b2656caff2ac2633d009b3a9ff7b5e93d0452f40cd76d3da3b3c"}, + {file = "fonttools-4.54.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d152d1be65652fc65e695e5619e0aa0982295a95a9b29b52b85775243c06556"}, + {file = "fonttools-4.54.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8583e563df41fdecef31b793b4dd3af8a9caa03397be648945ad32717a92885b"}, + {file = "fonttools-4.54.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0d1d353ef198c422515a3e974a1e8d5b304cd54a4c2eebcae708e37cd9eeffb1"}, + {file = "fonttools-4.54.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:fda582236fee135d4daeca056c8c88ec5f6f6d88a004a79b84a02547c8f57386"}, + {file = "fonttools-4.54.1-cp38-cp38-win32.whl", hash = "sha256:e7d82b9e56716ed32574ee106cabca80992e6bbdcf25a88d97d21f73a0aae664"}, + {file = "fonttools-4.54.1-cp38-cp38-win_amd64.whl", hash = "sha256:ada215fd079e23e060157aab12eba0d66704316547f334eee9ff26f8c0d7b8ab"}, + {file = "fonttools-4.54.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5b8a096e649768c2f4233f947cf9737f8dbf8728b90e2771e2497c6e3d21d13"}, + {file = "fonttools-4.54.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e10d2e0a12e18f4e2dd031e1bf7c3d7017be5c8dbe524d07706179f355c5dac"}, + {file = "fonttools-4.54.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31c32d7d4b0958600eac75eaf524b7b7cb68d3a8c196635252b7a2c30d80e986"}, + {file = "fonttools-4.54.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c39287f5c8f4a0c5a55daf9eaf9ccd223ea59eed3f6d467133cc727d7b943a55"}, + {file = "fonttools-4.54.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a7a310c6e0471602fe3bf8efaf193d396ea561486aeaa7adc1f132e02d30c4b9"}, + {file = "fonttools-4.54.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d3b659d1029946f4ff9b6183984578041b520ce0f8fb7078bb37ec7445806b33"}, + {file = "fonttools-4.54.1-cp39-cp39-win32.whl", hash = "sha256:e96bc94c8cda58f577277d4a71f51c8e2129b8b36fd05adece6320dd3d57de8a"}, + {file = "fonttools-4.54.1-cp39-cp39-win_amd64.whl", hash = "sha256:e8a4b261c1ef91e7188a30571be6ad98d1c6d9fa2427244c545e2fa0a2494dd7"}, + {file = "fonttools-4.54.1-py3-none-any.whl", hash = "sha256:37cddd62d83dc4f72f7c3f3c2bcf2697e89a30efb152079896544a93907733bd"}, + {file = "fonttools-4.54.1.tar.gz", hash = "sha256:957f669d4922f92c171ba01bef7f29410668db09f6c02111e22b2bce446f3285"}, +] + +[package.extras] +all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"] +graphite = ["lz4 (>=1.7.4.2)"] +interpolatable = ["munkres", "pycairo", "scipy"] +lxml = ["lxml (>=4.0)"] +pathops = ["skia-pathops (>=0.5.0)"] +plot = ["matplotlib"] +repacker = ["uharfbuzz (>=0.23.0)"] +symfont = ["sympy"] +type1 = ["xattr"] +ufo = ["fs (>=2.2.0,<3)"] +unicode = ["unicodedata2 (>=15.1.0)"] +woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] + [[package]] name = "frozenlist" version = "1.4.1" @@ -1497,13 +1839,13 @@ websockets = ["websockets (>=10,<12)"] [[package]] name = "graphql-core" -version = "3.2.4" +version = "3.2.5" description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." optional = false python-versions = "<4,>=3.6" files = [ - {file = "graphql-core-3.2.4.tar.gz", hash = "sha256:acbe2e800980d0e39b4685dd058c2f4042660b89ebca38af83020fd872ff1264"}, - {file = "graphql_core-3.2.4-py3-none-any.whl", hash = "sha256:1604f2042edc5f3114f49cac9d77e25863be51b23a54a61a23245cf32f6476f0"}, + {file = "graphql_core-3.2.5-py3-none-any.whl", hash = "sha256:2f150d5096448aa4f8ab26268567bbfeef823769893b39c1a2e1409590939c8a"}, + {file = "graphql_core-3.2.5.tar.gz", hash = "sha256:e671b90ed653c808715645e3998b7ab67d382d55467b7e2978549111bbabf8d5"}, ] [[package]] @@ -1563,6 +1905,17 @@ files = [ [package.extras] protobuf = ["grpcio-tools (>=1.53.0)"] +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + [[package]] name = "hexbytes" version = "0.3.1" @@ -1580,6 +1933,50 @@ doc = ["sphinx (>=5.0.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)"] lint = ["black (>=22)", "flake8 (==6.0.0)", "flake8-bugbear (==23.3.23)", "isort (>=5.10.1)", "mypy (==0.971)", "pydocstyle (>=5.0.0)"] test = ["eth-utils (>=1.0.1,<3)", "hypothesis (>=3.44.24,<=6.31.6)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] +[[package]] +name = "httpcore" +version = "0.16.3" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.7" +files = [ + {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, + {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, +] + +[package.dependencies] +anyio = ">=3.0,<5.0" +certifi = "*" +h11 = ">=0.13,<0.15" +sniffio = "==1.*" + +[package.extras] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + +[[package]] +name = "httpx" +version = "0.23.3" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.7" +files = [ + {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, + {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, +] + +[package.dependencies] +certifi = "*" +httpcore = ">=0.15.0,<0.17.0" +rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<13)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + [[package]] name = "hypothesis" version = "6.21.6" @@ -1653,18 +2050,15 @@ requests = ">=2.11" [[package]] name = "isodate" -version = "0.6.1" +version = "0.7.2" description = "An ISO 8601 date/time/duration parser and formatter" optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "isodate-0.6.1-py2.py3-none-any.whl", hash = "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96"}, - {file = "isodate-0.6.1.tar.gz", hash = "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9"}, + {file = "isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15"}, + {file = "isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6"}, ] -[package.dependencies] -six = "*" - [[package]] name = "isort" version = "5.13.2" @@ -1707,6 +2101,30 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "jsonalias" +version = "0.1.1" +description = "A microlibrary that defines a Json type alias for Python." +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "jsonalias-0.1.1-py3-none-any.whl", hash = "sha256:a56d2888e6397812c606156504e861e8ec00e188005af149f003c787db3d3f18"}, + {file = "jsonalias-0.1.1.tar.gz", hash = "sha256:64f04d935397d579fc94509e1fcb6212f2d081235d9d6395bd10baedf760a769"}, +] + +[[package]] +name = "jsonrpcclient" +version = "4.0.3" +description = "Send JSON-RPC requests" +optional = false +python-versions = ">=3.6" +files = [ + {file = "jsonrpcclient-4.0.3-py3-none-any.whl", hash = "sha256:3cbb9e27e1be29821becf135ea183144a836215422727e1ffe5056a49a670f0d"}, +] + +[package.extras] +qa = ["pytest", "pytest-cov", "tox"] + [[package]] name = "jsonschema" version = "4.3.3" @@ -1726,6 +2144,139 @@ pyrsistent = ">=0.14.0,<0.17.0 || >0.17.0,<0.17.1 || >0.17.1,<0.17.2 || >0.17.2" format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] +[[package]] +name = "jstyleson" +version = "0.0.2" +description = "Library to parse JSON with js-style comments." +optional = false +python-versions = "*" +files = [ + {file = "jstyleson-0.0.2.tar.gz", hash = "sha256:680003f3b15a2959e4e6a351f3b858e3c07dd3e073a0d54954e34d8ea5e1308e"}, +] + +[[package]] +name = "kiwisolver" +version = "1.4.7" +description = "A fast implementation of the Cassowary constraint solver" +optional = false +python-versions = ">=3.8" +files = [ + {file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6"}, + {file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17"}, + {file = "kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9"}, + {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9"}, + {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c"}, + {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599"}, + {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05"}, + {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407"}, + {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278"}, + {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5"}, + {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad"}, + {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895"}, + {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3"}, + {file = "kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc"}, + {file = "kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c"}, + {file = "kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a"}, + {file = "kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54"}, + {file = "kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95"}, + {file = "kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935"}, + {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb"}, + {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02"}, + {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51"}, + {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052"}, + {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18"}, + {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545"}, + {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b"}, + {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36"}, + {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3"}, + {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523"}, + {file = "kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d"}, + {file = "kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b"}, + {file = "kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376"}, + {file = "kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2"}, + {file = "kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a"}, + {file = "kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee"}, + {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640"}, + {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f"}, + {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483"}, + {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258"}, + {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e"}, + {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107"}, + {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948"}, + {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038"}, + {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383"}, + {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520"}, + {file = "kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b"}, + {file = "kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb"}, + {file = "kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a"}, + {file = "kiwisolver-1.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e"}, + {file = "kiwisolver-1.4.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6"}, + {file = "kiwisolver-1.4.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750"}, + {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d"}, + {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379"}, + {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c"}, + {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34"}, + {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1"}, + {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f"}, + {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b"}, + {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27"}, + {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a"}, + {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee"}, + {file = "kiwisolver-1.4.7-cp313-cp313-win32.whl", hash = "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07"}, + {file = "kiwisolver-1.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76"}, + {file = "kiwisolver-1.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650"}, + {file = "kiwisolver-1.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5d5abf8f8ec1f4e22882273c423e16cae834c36856cac348cfbfa68e01c40f3a"}, + {file = "kiwisolver-1.4.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:aeb3531b196ef6f11776c21674dba836aeea9d5bd1cf630f869e3d90b16cfade"}, + {file = "kiwisolver-1.4.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7d755065e4e866a8086c9bdada157133ff466476a2ad7861828e17b6026e22c"}, + {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08471d4d86cbaec61f86b217dd938a83d85e03785f51121e791a6e6689a3be95"}, + {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bbfcb7165ce3d54a3dfbe731e470f65739c4c1f85bb1018ee912bae139e263b"}, + {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d34eb8494bea691a1a450141ebb5385e4b69d38bb8403b5146ad279f4b30fa3"}, + {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9242795d174daa40105c1d86aba618e8eab7bf96ba8c3ee614da8302a9f95503"}, + {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a0f64a48bb81af7450e641e3fe0b0394d7381e342805479178b3d335d60ca7cf"}, + {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8e045731a5416357638d1700927529e2b8ab304811671f665b225f8bf8d8f933"}, + {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4322872d5772cae7369f8351da1edf255a604ea7087fe295411397d0cfd9655e"}, + {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e1631290ee9271dffe3062d2634c3ecac02c83890ada077d225e081aca8aab89"}, + {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:edcfc407e4eb17e037bca59be0e85a2031a2ac87e4fed26d3e9df88b4165f92d"}, + {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4d05d81ecb47d11e7f8932bd8b61b720bf0b41199358f3f5e36d38e28f0532c5"}, + {file = "kiwisolver-1.4.7-cp38-cp38-win32.whl", hash = "sha256:b38ac83d5f04b15e515fd86f312479d950d05ce2368d5413d46c088dda7de90a"}, + {file = "kiwisolver-1.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:d83db7cde68459fc803052a55ace60bea2bae361fc3b7a6d5da07e11954e4b09"}, + {file = "kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd"}, + {file = "kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583"}, + {file = "kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417"}, + {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904"}, + {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a"}, + {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8"}, + {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2"}, + {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88"}, + {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde"}, + {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c"}, + {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2"}, + {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb"}, + {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327"}, + {file = "kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644"}, + {file = "kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4"}, + {file = "kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bfa1acfa0c54932d5607e19a2c24646fb4c1ae2694437789129cf099789a3b00"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:eee3ea935c3d227d49b4eb85660ff631556841f6e567f0f7bda972df6c2c9935"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f3160309af4396e0ed04db259c3ccbfdc3621b5559b5453075e5de555e1f3a1b"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a17f6a29cf8935e587cc8a4dbfc8368c55edc645283db0ce9801016f83526c2d"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10849fb2c1ecbfae45a693c070e0320a91b35dd4bcf58172c023b994283a124d"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:ac542bf38a8a4be2dc6b15248d36315ccc65f0743f7b1a76688ffb6b5129a5c2"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0"}, + {file = "kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60"}, +] + [[package]] name = "lru-dict" version = "1.2.0" @@ -1820,84 +2371,203 @@ files = [ [package.extras] test = ["pytest"] +[[package]] +name = "lyra-v2-client" +version = "0.2.10" +description = "" +optional = false +python-versions = "<=3.11.9,>=3.8.1" +files = [ + {file = "lyra_v2_client-0.2.10-py3-none-any.whl", hash = "sha256:c10f9d38227e43294bfbabce4ecfd0460e82ba05e2fad0e28e739c0140c843a9"}, + {file = "lyra_v2_client-0.2.10.tar.gz", hash = "sha256:6fbb200c9f8ae9c9b26c6a1d7de3ef6bc84226a8a1942af08b7f48981187fa1c"}, +] + +[package.dependencies] +pandas = ">=1,<2" +python-dotenv = ">=0.14.0,<0.18.0" +requests = ">=2,<3" +rich-click = ">=1.7.1,<2.0.0" +setuptools = ">=68.2.2,<70" +web3 = ">=6,<7" +websocket-client = ">=0.32.0,<1" + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + [[package]] name = "markupsafe" -version = "2.1.5" +version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false +python-versions = ">=3.9" +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "matplotlib" +version = "3.9.2" +description = "Python plotting package" +optional = false +python-versions = ">=3.9" +files = [ + {file = "matplotlib-3.9.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9d78bbc0cbc891ad55b4f39a48c22182e9bdaea7fc0e5dbd364f49f729ca1bbb"}, + {file = "matplotlib-3.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c375cc72229614632c87355366bdf2570c2dac01ac66b8ad048d2dabadf2d0d4"}, + {file = "matplotlib-3.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d94ff717eb2bd0b58fe66380bd8b14ac35f48a98e7c6765117fe67fb7684e64"}, + {file = "matplotlib-3.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab68d50c06938ef28681073327795c5db99bb4666214d2d5f880ed11aeaded66"}, + {file = "matplotlib-3.9.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:65aacf95b62272d568044531e41de26285d54aec8cb859031f511f84bd8b495a"}, + {file = "matplotlib-3.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:3fd595f34aa8a55b7fc8bf9ebea8aa665a84c82d275190a61118d33fbc82ccae"}, + {file = "matplotlib-3.9.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8dd059447824eec055e829258ab092b56bb0579fc3164fa09c64f3acd478772"}, + {file = "matplotlib-3.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c797dac8bb9c7a3fd3382b16fe8f215b4cf0f22adccea36f1545a6d7be310b41"}, + {file = "matplotlib-3.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d719465db13267bcef19ea8954a971db03b9f48b4647e3860e4bc8e6ed86610f"}, + {file = "matplotlib-3.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8912ef7c2362f7193b5819d17dae8629b34a95c58603d781329712ada83f9447"}, + {file = "matplotlib-3.9.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7741f26a58a240f43bee74965c4882b6c93df3e7eb3de160126d8c8f53a6ae6e"}, + {file = "matplotlib-3.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:ae82a14dab96fbfad7965403c643cafe6515e386de723e498cf3eeb1e0b70cc7"}, + {file = "matplotlib-3.9.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ac43031375a65c3196bee99f6001e7fa5bdfb00ddf43379d3c0609bdca042df9"}, + {file = "matplotlib-3.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be0fc24a5e4531ae4d8e858a1a548c1fe33b176bb13eff7f9d0d38ce5112a27d"}, + {file = "matplotlib-3.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf81de2926c2db243c9b2cbc3917619a0fc85796c6ba4e58f541df814bbf83c7"}, + {file = "matplotlib-3.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6ee45bc4245533111ced13f1f2cace1e7f89d1c793390392a80c139d6cf0e6c"}, + {file = "matplotlib-3.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:306c8dfc73239f0e72ac50e5a9cf19cc4e8e331dd0c54f5e69ca8758550f1e1e"}, + {file = "matplotlib-3.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:5413401594cfaff0052f9d8b1aafc6d305b4bd7c4331dccd18f561ff7e1d3bd3"}, + {file = "matplotlib-3.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:18128cc08f0d3cfff10b76baa2f296fc28c4607368a8402de61bb3f2eb33c7d9"}, + {file = "matplotlib-3.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4876d7d40219e8ae8bb70f9263bcbe5714415acfdf781086601211335e24f8aa"}, + {file = "matplotlib-3.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d9f07a80deab4bb0b82858a9e9ad53d1382fd122be8cde11080f4e7dfedb38b"}, + {file = "matplotlib-3.9.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7c0410f181a531ec4e93bbc27692f2c71a15c2da16766f5ba9761e7ae518413"}, + {file = "matplotlib-3.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:909645cce2dc28b735674ce0931a4ac94e12f5b13f6bb0b5a5e65e7cea2c192b"}, + {file = "matplotlib-3.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:f32c7410c7f246838a77d6d1eff0c0f87f3cb0e7c4247aebea71a6d5a68cab49"}, + {file = "matplotlib-3.9.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:37e51dd1c2db16ede9cfd7b5cabdfc818b2c6397c83f8b10e0e797501c963a03"}, + {file = "matplotlib-3.9.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b82c5045cebcecd8496a4d694d43f9cc84aeeb49fe2133e036b207abe73f4d30"}, + {file = "matplotlib-3.9.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f053c40f94bc51bc03832a41b4f153d83f2062d88c72b5e79997072594e97e51"}, + {file = "matplotlib-3.9.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbe196377a8248972f5cede786d4c5508ed5f5ca4a1e09b44bda889958b33f8c"}, + {file = "matplotlib-3.9.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5816b1e1fe8c192cbc013f8f3e3368ac56fbecf02fb41b8f8559303f24c5015e"}, + {file = "matplotlib-3.9.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:cef2a73d06601437be399908cf13aee74e86932a5ccc6ccdf173408ebc5f6bb2"}, + {file = "matplotlib-3.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e0830e188029c14e891fadd99702fd90d317df294c3298aad682739c5533721a"}, + {file = "matplotlib-3.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ba9c1299c920964e8d3857ba27173b4dbb51ca4bab47ffc2c2ba0eb5e2cbc5"}, + {file = "matplotlib-3.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cd93b91ab47a3616b4d3c42b52f8363b88ca021e340804c6ab2536344fad9ca"}, + {file = "matplotlib-3.9.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6d1ce5ed2aefcdce11904fc5bbea7d9c21fff3d5f543841edf3dea84451a09ea"}, + {file = "matplotlib-3.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:b2696efdc08648536efd4e1601b5fd491fd47f4db97a5fbfd175549a7365c1b2"}, + {file = "matplotlib-3.9.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d52a3b618cb1cbb769ce2ee1dcdb333c3ab6e823944e9a2d36e37253815f9556"}, + {file = "matplotlib-3.9.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:039082812cacd6c6bec8e17a9c1e6baca230d4116d522e81e1f63a74d01d2e21"}, + {file = "matplotlib-3.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6758baae2ed64f2331d4fd19be38b7b4eae3ecec210049a26b6a4f3ae1c85dcc"}, + {file = "matplotlib-3.9.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:050598c2b29e0b9832cde72bcf97627bf00262adbc4a54e2b856426bb2ef0697"}, + {file = "matplotlib-3.9.2.tar.gz", hash = "sha256:96ab43906269ca64a6366934106fa01534454a69e471b7bf3d79083981aaab92"}, +] + +[package.dependencies] +contourpy = ">=1.0.1" +cycler = ">=0.10" +fonttools = ">=4.22.0" +kiwisolver = ">=1.3.1" +numpy = ">=1.23" +packaging = ">=20.0" +pillow = ">=8" +pyparsing = ">=2.3.1" +python-dateutil = ">=2.7" + +[package.extras] +dev = ["meson-python (>=0.13.1)", "numpy (>=1.25)", "pybind11 (>=2.6)", "setuptools (>=64)", "setuptools_scm (>=7)"] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false python-versions = ">=3.7" files = [ - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, - {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] [[package]] name = "more-itertools" -version = "10.5.0" +version = "8.14.0" description = "More routines for operating on iterables, beyond itertools" optional = false -python-versions = ">=3.8" +python-versions = ">=3.5" files = [ - {file = "more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6"}, - {file = "more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef"}, + {file = "more-itertools-8.14.0.tar.gz", hash = "sha256:c09443cd3d5438b8dafccd867a6bc1cb0894389e90cb53d227456b0b0bccb750"}, + {file = "more_itertools-8.14.0-py3-none-any.whl", hash = "sha256:1bc4f91ee5b1b31ac7ceacc17c09befe6a40a503907baf9c839c229b5095cfd2"}, ] [[package]] @@ -1927,6 +2597,22 @@ netaddr = "*" six = "*" varint = "*" +[[package]] +name = "multicaller" +version = "0.1.7" +description = "web3py multicaller simplified interface" +optional = false +python-versions = ">=3.8.1,<4" +files = [] +develop = false + +[package.dependencies] +web3 = "~6" + +[package.source] +type = "directory" +url = "third_party/multicaller" + [[package]] name = "multidict" version = "6.1.0" @@ -2056,6 +2742,63 @@ files = [ [package.extras] nicer-shell = ["ipython"] +[[package]] +name = "numpy" +version = "1.26.1" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "numpy-1.26.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82e871307a6331b5f09efda3c22e03c095d957f04bf6bc1804f30048d0e5e7af"}, + {file = "numpy-1.26.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdd9ec98f0063d93baeb01aad472a1a0840dee302842a2746a7a8e92968f9575"}, + {file = "numpy-1.26.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d78f269e0c4fd365fc2992c00353e4530d274ba68f15e968d8bc3c69ce5f5244"}, + {file = "numpy-1.26.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ab9163ca8aeb7fd32fe93866490654d2f7dda4e61bc6297bf72ce07fdc02f67"}, + {file = "numpy-1.26.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:78ca54b2f9daffa5f323f34cdf21e1d9779a54073f0018a3094ab907938331a2"}, + {file = "numpy-1.26.1-cp310-cp310-win32.whl", hash = "sha256:d1cfc92db6af1fd37a7bb58e55c8383b4aa1ba23d012bdbba26b4bcca45ac297"}, + {file = "numpy-1.26.1-cp310-cp310-win_amd64.whl", hash = "sha256:d2984cb6caaf05294b8466966627e80bf6c7afd273279077679cb010acb0e5ab"}, + {file = "numpy-1.26.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cd7837b2b734ca72959a1caf3309457a318c934abef7a43a14bb984e574bbb9a"}, + {file = "numpy-1.26.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1c59c046c31a43310ad0199d6299e59f57a289e22f0f36951ced1c9eac3665b9"}, + {file = "numpy-1.26.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d58e8c51a7cf43090d124d5073bc29ab2755822181fcad978b12e144e5e5a4b3"}, + {file = "numpy-1.26.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6081aed64714a18c72b168a9276095ef9155dd7888b9e74b5987808f0dd0a974"}, + {file = "numpy-1.26.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:97e5d6a9f0702c2863aaabf19f0d1b6c2628fbe476438ce0b5ce06e83085064c"}, + {file = "numpy-1.26.1-cp311-cp311-win32.whl", hash = "sha256:b9d45d1dbb9de84894cc50efece5b09939752a2d75aab3a8b0cef6f3a35ecd6b"}, + {file = "numpy-1.26.1-cp311-cp311-win_amd64.whl", hash = "sha256:3649d566e2fc067597125428db15d60eb42a4e0897fc48d28cb75dc2e0454e53"}, + {file = "numpy-1.26.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1d1bd82d539607951cac963388534da3b7ea0e18b149a53cf883d8f699178c0f"}, + {file = "numpy-1.26.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:afd5ced4e5a96dac6725daeb5242a35494243f2239244fad10a90ce58b071d24"}, + {file = "numpy-1.26.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03fb25610ef560a6201ff06df4f8105292ba56e7cdd196ea350d123fc32e24e"}, + {file = "numpy-1.26.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcfaf015b79d1f9f9c9fd0731a907407dc3e45769262d657d754c3a028586124"}, + {file = "numpy-1.26.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e509cbc488c735b43b5ffea175235cec24bbc57b227ef1acc691725beb230d1c"}, + {file = "numpy-1.26.1-cp312-cp312-win32.whl", hash = "sha256:af22f3d8e228d84d1c0c44c1fbdeb80f97a15a0abe4f080960393a00db733b66"}, + {file = "numpy-1.26.1-cp312-cp312-win_amd64.whl", hash = "sha256:9f42284ebf91bdf32fafac29d29d4c07e5e9d1af862ea73686581773ef9e73a7"}, + {file = "numpy-1.26.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bb894accfd16b867d8643fc2ba6c8617c78ba2828051e9a69511644ce86ce83e"}, + {file = "numpy-1.26.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e44ccb93f30c75dfc0c3aa3ce38f33486a75ec9abadabd4e59f114994a9c4617"}, + {file = "numpy-1.26.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9696aa2e35cc41e398a6d42d147cf326f8f9d81befcb399bc1ed7ffea339b64e"}, + {file = "numpy-1.26.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5b411040beead47a228bde3b2241100454a6abde9df139ed087bd73fc0a4908"}, + {file = "numpy-1.26.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1e11668d6f756ca5ef534b5be8653d16c5352cbb210a5c2a79ff288e937010d5"}, + {file = "numpy-1.26.1-cp39-cp39-win32.whl", hash = "sha256:d1d2c6b7dd618c41e202c59c1413ef9b2c8e8a15f5039e344af64195459e3104"}, + {file = "numpy-1.26.1-cp39-cp39-win_amd64.whl", hash = "sha256:59227c981d43425ca5e5c01094d59eb14e8772ce6975d4b2fc1e106a833d5ae2"}, + {file = "numpy-1.26.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:06934e1a22c54636a059215d6da99e23286424f316fddd979f5071093b648668"}, + {file = "numpy-1.26.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76ff661a867d9272cd2a99eed002470f46dbe0943a5ffd140f49be84f68ffc42"}, + {file = "numpy-1.26.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:6965888d65d2848e8768824ca8288db0a81263c1efccec881cb35a0d805fcd2f"}, + {file = "numpy-1.26.1.tar.gz", hash = "sha256:c8c6c72d4a9f831f328efb1312642a1cafafaa88981d9ab76368d50d07d93cbe"}, +] + +[[package]] +name = "oauthlib" +version = "3.2.2" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +optional = false +python-versions = ">=3.6" +files = [ + {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, + {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, +] + +[package.extras] +rsa = ["cryptography (>=3.0.0)"] +signals = ["blinker (>=1.4.0)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] + [[package]] name = "open-aea" version = "1.57.0" @@ -2146,6 +2889,25 @@ ipfshttpclient = "0.8.0a2" open-aea = ">=1.0.0,<2.0.0" web3 = ">=6.0.0,<7" +[[package]] +name = "open-aea-ledger-solana" +version = "1.57.0" +description = "Python package wrapping the public and private key cryptography and ledger api of solana." +optional = false +python-versions = "*" +files = [ + {file = "open_aea_ledger_solana-1.57.0-py3-none-any.whl", hash = "sha256:a32f26480a9e44a85edb3bcd3fb47584e20871c5195c16ef468a8836ed2b8820"}, + {file = "open_aea_ledger_solana-1.57.0.tar.gz", hash = "sha256:00faaf0d9425044a8665153fdb82801bd00cc4fb938a186dec8a30e808eb692f"}, +] + +[package.dependencies] +anchorpy = ">=0.17.0,<0.19.0" +cryptography = "*" +open-aea = ">=1.0.0,<2.0.0" +PyNaCl = ">=1.5.0,<2" +solana = ">=0.29.0" +solders = ">=0.14.0" + [[package]] name = "open-aea-test-autonomy" version = "0.16.1" @@ -2277,6 +3039,53 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] +[[package]] +name = "pandas" +version = "1.5.3" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3749077d86e3a2f0ed51367f30bf5b82e131cc0f14260c4d3e499186fccc4406"}, + {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:972d8a45395f2a2d26733eb8d0f629b2f90bebe8e8eddbb8829b180c09639572"}, + {file = "pandas-1.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50869a35cbb0f2e0cd5ec04b191e7b12ed688874bd05dd777c19b28cbea90996"}, + {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3ac844a0fe00bfaeb2c9b51ab1424e5c8744f89860b138434a363b1f620f354"}, + {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0a56cef15fd1586726dace5616db75ebcfec9179a3a55e78f72c5639fa2a23"}, + {file = "pandas-1.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:478ff646ca42b20376e4ed3fa2e8d7341e8a63105586efe54fa2508ee087f328"}, + {file = "pandas-1.5.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6973549c01ca91ec96199e940495219c887ea815b2083722821f1d7abfa2b4dc"}, + {file = "pandas-1.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c39a8da13cede5adcd3be1182883aea1c925476f4e84b2807a46e2775306305d"}, + {file = "pandas-1.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f76d097d12c82a535fda9dfe5e8dd4127952b45fea9b0276cb30cca5ea313fbc"}, + {file = "pandas-1.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e474390e60ed609cec869b0da796ad94f420bb057d86784191eefc62b65819ae"}, + {file = "pandas-1.5.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f2b952406a1588ad4cad5b3f55f520e82e902388a6d5a4a91baa8d38d23c7f6"}, + {file = "pandas-1.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc4c368f42b551bf72fac35c5128963a171b40dce866fb066540eeaf46faa003"}, + {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:14e45300521902689a81f3f41386dc86f19b8ba8dd5ac5a3c7010ef8d2932813"}, + {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9842b6f4b8479e41968eced654487258ed81df7d1c9b7b870ceea24ed9459b31"}, + {file = "pandas-1.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:26d9c71772c7afb9d5046e6e9cf42d83dd147b5cf5bcb9d97252077118543792"}, + {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fbcb19d6fceb9e946b3e23258757c7b225ba450990d9ed63ccceeb8cae609f7"}, + {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:565fa34a5434d38e9d250af3c12ff931abaf88050551d9fbcdfafca50d62babf"}, + {file = "pandas-1.5.3-cp38-cp38-win32.whl", hash = "sha256:87bd9c03da1ac870a6d2c8902a0e1fd4267ca00f13bc494c9e5a9020920e1d51"}, + {file = "pandas-1.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:41179ce559943d83a9b4bbacb736b04c928b095b5f25dd2b7389eda08f46f373"}, + {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c74a62747864ed568f5a82a49a23a8d7fe171d0c69038b38cedf0976831296fa"}, + {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c4c00e0b0597c8e4f59e8d461f797e5d70b4d025880516a8261b2817c47759ee"}, + {file = "pandas-1.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a50d9a4336a9621cab7b8eb3fb11adb82de58f9b91d84c2cd526576b881a0c5a"}, + {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd05f7783b3274aa206a1af06f0ceed3f9b412cf665b7247eacd83be41cf7bf0"}, + {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f69c4029613de47816b1bb30ff5ac778686688751a5e9c99ad8c7031f6508e5"}, + {file = "pandas-1.5.3-cp39-cp39-win32.whl", hash = "sha256:7cec0bee9f294e5de5bbfc14d0573f65526071029d036b753ee6507d2a21480a"}, + {file = "pandas-1.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:dfd681c5dc216037e0b0a2c821f5ed99ba9f03ebcf119c7dac0e9a7b960b9ec9"}, + {file = "pandas-1.5.3.tar.gz", hash = "sha256:74a3fd7e5a7ec052f183273dc7b0acd3a863edf7520f5d3a1765c04ffdb3b0b1"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.21.0", markers = "python_version >= \"3.10\" and python_version < \"3.11\""}, + {version = ">=1.23.2", markers = "python_version >= \"3.11\""}, +] +python-dateutil = ">=2.8.1" +pytz = ">=2020.1" + +[package.extras] +test = ["hypothesis (>=5.5.3)", "pytest (>=6.0)", "pytest-xdist (>=1.31)"] + [[package]] name = "paramiko" version = "3.5.0" @@ -2311,13 +3120,12 @@ files = [ [[package]] name = "parsimonious" -version = "0.10.0" +version = "0.9.0" description = "(Soon to be) the fastest pure-Python PEG parser I could muster" optional = false python-versions = "*" files = [ - {file = "parsimonious-0.10.0-py3-none-any.whl", hash = "sha256:982ab435fabe86519b57f6b35610aa4e4e977e9f02a14353edf4bbc75369fc0f"}, - {file = "parsimonious-0.10.0.tar.gz", hash = "sha256:8281600da180ec8ae35427a4ab4f7b82bfec1e3d1e52f80cb60ea82b9512501c"}, + {file = "parsimonious-0.9.0.tar.gz", hash = "sha256:b2ad1ae63a2f65bd78f5e0a8ac510a98f3607a43f1db2a8d46636a5d9e4a30c1"}, ] [package.dependencies] @@ -2345,6 +3153,98 @@ files = [ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] +[[package]] +name = "pillow" +version = "11.0.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pillow-11.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947"}, + {file = "pillow-11.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f"}, + {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb"}, + {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97"}, + {file = "pillow-11.0.0-cp310-cp310-win32.whl", hash = "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50"}, + {file = "pillow-11.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c"}, + {file = "pillow-11.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1"}, + {file = "pillow-11.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc"}, + {file = "pillow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa"}, + {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306"}, + {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9"}, + {file = "pillow-11.0.0-cp311-cp311-win32.whl", hash = "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5"}, + {file = "pillow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291"}, + {file = "pillow-11.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9"}, + {file = "pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923"}, + {file = "pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7"}, + {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6"}, + {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc"}, + {file = "pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6"}, + {file = "pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47"}, + {file = "pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25"}, + {file = "pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699"}, + {file = "pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa"}, + {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f"}, + {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb"}, + {file = "pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798"}, + {file = "pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de"}, + {file = "pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84"}, + {file = "pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b"}, + {file = "pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003"}, + {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2"}, + {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a"}, + {file = "pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8"}, + {file = "pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8"}, + {file = "pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904"}, + {file = "pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3"}, + {file = "pillow-11.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba"}, + {file = "pillow-11.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e"}, + {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f"}, + {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae"}, + {file = "pillow-11.0.0-cp39-cp39-win32.whl", hash = "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4"}, + {file = "pillow-11.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd"}, + {file = "pillow-11.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944"}, + {file = "pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + [[package]] name = "platformdirs" version = "4.3.6" @@ -2376,6 +3276,113 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "propcache" +version = "0.2.0" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.8" +files = [ + {file = "propcache-0.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c5869b8fd70b81835a6f187c5fdbe67917a04d7e52b6e7cc4e5fe39d55c39d58"}, + {file = "propcache-0.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:952e0d9d07609d9c5be361f33b0d6d650cd2bae393aabb11d9b719364521984b"}, + {file = "propcache-0.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:33ac8f098df0585c0b53009f039dfd913b38c1d2edafed0cedcc0c32a05aa110"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e48e8875e6c13909c800fa344cd54cc4b2b0db1d5f911f840458a500fde2c2"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:388f3217649d6d59292b722d940d4d2e1e6a7003259eb835724092a1cca0203a"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f571aea50ba5623c308aa146eb650eebf7dbe0fd8c5d946e28343cb3b5aad577"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3dfafb44f7bb35c0c06eda6b2ab4bfd58f02729e7c4045e179f9a861b07c9850"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3ebe9a75be7ab0b7da2464a77bb27febcb4fab46a34f9288f39d74833db7f61"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d2f0d0f976985f85dfb5f3d685697ef769faa6b71993b46b295cdbbd6be8cc37"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a3dc1a4b165283bd865e8f8cb5f0c64c05001e0718ed06250d8cac9bec115b48"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9e0f07b42d2a50c7dd2d8675d50f7343d998c64008f1da5fef888396b7f84630"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e63e3e1e0271f374ed489ff5ee73d4b6e7c60710e1f76af5f0e1a6117cd26394"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:56bb5c98f058a41bb58eead194b4db8c05b088c93d94d5161728515bd52b052b"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7665f04d0c7f26ff8bb534e1c65068409bf4687aa2534faf7104d7182debb336"}, + {file = "propcache-0.2.0-cp310-cp310-win32.whl", hash = "sha256:7cf18abf9764746b9c8704774d8b06714bcb0a63641518a3a89c7f85cc02c2ad"}, + {file = "propcache-0.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:cfac69017ef97db2438efb854edf24f5a29fd09a536ff3a992b75990720cdc99"}, + {file = "propcache-0.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:63f13bf09cc3336eb04a837490b8f332e0db41da66995c9fd1ba04552e516354"}, + {file = "propcache-0.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608cce1da6f2672a56b24a015b42db4ac612ee709f3d29f27a00c943d9e851de"}, + {file = "propcache-0.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:466c219deee4536fbc83c08d09115249db301550625c7fef1c5563a584c9bc87"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc2db02409338bf36590aa985a461b2c96fce91f8e7e0f14c50c5fcc4f229016"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6ed8db0a556343d566a5c124ee483ae113acc9a557a807d439bcecc44e7dfbb"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91997d9cb4a325b60d4e3f20967f8eb08dfcb32b22554d5ef78e6fd1dda743a2"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c7dde9e533c0a49d802b4f3f218fa9ad0a1ce21f2c2eb80d5216565202acab4"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffcad6c564fe6b9b8916c1aefbb37a362deebf9394bd2974e9d84232e3e08504"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:97a58a28bcf63284e8b4d7b460cbee1edaab24634e82059c7b8c09e65284f178"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:945db8ee295d3af9dbdbb698cce9bbc5c59b5c3fe328bbc4387f59a8a35f998d"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39e104da444a34830751715f45ef9fc537475ba21b7f1f5b0f4d71a3b60d7fe2"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c5ecca8f9bab618340c8e848d340baf68bcd8ad90a8ecd7a4524a81c1764b3db"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c436130cc779806bdf5d5fae0d848713105472b8566b75ff70048c47d3961c5b"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:191db28dc6dcd29d1a3e063c3be0b40688ed76434622c53a284e5427565bbd9b"}, + {file = "propcache-0.2.0-cp311-cp311-win32.whl", hash = "sha256:5f2564ec89058ee7c7989a7b719115bdfe2a2fb8e7a4543b8d1c0cc4cf6478c1"}, + {file = "propcache-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e2e54267980349b723cff366d1e29b138b9a60fa376664a157a342689553f71"}, + {file = "propcache-0.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ee7606193fb267be4b2e3b32714f2d58cad27217638db98a60f9efb5efeccc2"}, + {file = "propcache-0.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:91ee8fc02ca52e24bcb77b234f22afc03288e1dafbb1f88fe24db308910c4ac7"}, + {file = "propcache-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e900bad2a8456d00a113cad8c13343f3b1f327534e3589acc2219729237a2e8"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f52a68c21363c45297aca15561812d542f8fc683c85201df0bebe209e349f793"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e41d67757ff4fbc8ef2af99b338bfb955010444b92929e9e55a6d4dcc3c4f09"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a64e32f8bd94c105cc27f42d3b658902b5bcc947ece3c8fe7bc1b05982f60e89"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55346705687dbd7ef0d77883ab4f6fabc48232f587925bdaf95219bae072491e"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00181262b17e517df2cd85656fcd6b4e70946fe62cd625b9d74ac9977b64d8d9"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6994984550eaf25dd7fc7bd1b700ff45c894149341725bb4edc67f0ffa94efa4"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:56295eb1e5f3aecd516d91b00cfd8bf3a13991de5a479df9e27dd569ea23959c"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:439e76255daa0f8151d3cb325f6dd4a3e93043e6403e6491813bcaaaa8733887"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f6475a1b2ecb310c98c28d271a30df74f9dd436ee46d09236a6b750a7599ce57"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3444cdba6628accf384e349014084b1cacd866fbb88433cd9d279d90a54e0b23"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a9d9b4d0a9b38d1c391bb4ad24aa65f306c6f01b512e10a8a34a2dc5675d348"}, + {file = "propcache-0.2.0-cp312-cp312-win32.whl", hash = "sha256:69d3a98eebae99a420d4b28756c8ce6ea5a29291baf2dc9ff9414b42676f61d5"}, + {file = "propcache-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ad9c9b99b05f163109466638bd30ada1722abb01bbb85c739c50b6dc11f92dc3"}, + {file = "propcache-0.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ecddc221a077a8132cf7c747d5352a15ed763b674c0448d811f408bf803d9ad7"}, + {file = "propcache-0.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0e53cb83fdd61cbd67202735e6a6687a7b491c8742dfc39c9e01e80354956763"}, + {file = "propcache-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92fe151145a990c22cbccf9ae15cae8ae9eddabfc949a219c9f667877e40853d"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a21ef516d36909931a2967621eecb256018aeb11fc48656e3257e73e2e247a"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f88a4095e913f98988f5b338c1d4d5d07dbb0b6bad19892fd447484e483ba6b"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a5b3bb545ead161be780ee85a2b54fdf7092815995661947812dde94a40f6fb"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67aeb72e0f482709991aa91345a831d0b707d16b0257e8ef88a2ad246a7280bf"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c997f8c44ec9b9b0bcbf2d422cc00a1d9b9c681f56efa6ca149a941e5560da2"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a66df3d4992bc1d725b9aa803e8c5a66c010c65c741ad901e260ece77f58d2f"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3ebbcf2a07621f29638799828b8d8668c421bfb94c6cb04269130d8de4fb7136"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1235c01ddaa80da8235741e80815ce381c5267f96cc49b1477fdcf8c047ef325"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3947483a381259c06921612550867b37d22e1df6d6d7e8361264b6d037595f44"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d5bed7f9805cc29c780f3aee05de3262ee7ce1f47083cfe9f77471e9d6777e83"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4a91d44379f45f5e540971d41e4626dacd7f01004826a18cb048e7da7e96544"}, + {file = "propcache-0.2.0-cp313-cp313-win32.whl", hash = "sha256:f902804113e032e2cdf8c71015651c97af6418363bea8d78dc0911d56c335032"}, + {file = "propcache-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8f188cfcc64fb1266f4684206c9de0e80f54622c3f22a910cbd200478aeae61e"}, + {file = "propcache-0.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:53d1bd3f979ed529f0805dd35ddaca330f80a9a6d90bc0121d2ff398f8ed8861"}, + {file = "propcache-0.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:83928404adf8fb3d26793665633ea79b7361efa0287dfbd372a7e74311d51ee6"}, + {file = "propcache-0.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:77a86c261679ea5f3896ec060be9dc8e365788248cc1e049632a1be682442063"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:218db2a3c297a3768c11a34812e63b3ac1c3234c3a086def9c0fee50d35add1f"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7735e82e3498c27bcb2d17cb65d62c14f1100b71723b68362872bca7d0913d90"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:20a617c776f520c3875cf4511e0d1db847a076d720714ae35ffe0df3e440be68"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67b69535c870670c9f9b14a75d28baa32221d06f6b6fa6f77a0a13c5a7b0a5b9"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4569158070180c3855e9c0791c56be3ceeb192defa2cdf6a3f39e54319e56b89"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:db47514ffdbd91ccdc7e6f8407aac4ee94cc871b15b577c1c324236b013ddd04"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:2a60ad3e2553a74168d275a0ef35e8c0a965448ffbc3b300ab3a5bb9956c2162"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:662dd62358bdeaca0aee5761de8727cfd6861432e3bb828dc2a693aa0471a563"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:25a1f88b471b3bc911d18b935ecb7115dff3a192b6fef46f0bfaf71ff4f12418"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:f60f0ac7005b9f5a6091009b09a419ace1610e163fa5deaba5ce3484341840e7"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:74acd6e291f885678631b7ebc85d2d4aec458dd849b8c841b57ef04047833bed"}, + {file = "propcache-0.2.0-cp38-cp38-win32.whl", hash = "sha256:d9b6ddac6408194e934002a69bcaadbc88c10b5f38fb9307779d1c629181815d"}, + {file = "propcache-0.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:676135dcf3262c9c5081cc8f19ad55c8a64e3f7282a21266d05544450bffc3a5"}, + {file = "propcache-0.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:25c8d773a62ce0451b020c7b29a35cfbc05de8b291163a7a0f3b7904f27253e6"}, + {file = "propcache-0.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:375a12d7556d462dc64d70475a9ee5982465fbb3d2b364f16b86ba9135793638"}, + {file = "propcache-0.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1ec43d76b9677637a89d6ab86e1fef70d739217fefa208c65352ecf0282be957"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f45eec587dafd4b2d41ac189c2156461ebd0c1082d2fe7013571598abb8505d1"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc092ba439d91df90aea38168e11f75c655880c12782facf5cf9c00f3d42b562"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa1076244f54bb76e65e22cb6910365779d5c3d71d1f18b275f1dfc7b0d71b4d"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:682a7c79a2fbf40f5dbb1eb6bfe2cd865376deeac65acf9beb607505dced9e12"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e40876731f99b6f3c897b66b803c9e1c07a989b366c6b5b475fafd1f7ba3fb8"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:363ea8cd3c5cb6679f1c2f5f1f9669587361c062e4899fce56758efa928728f8"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:140fbf08ab3588b3468932974a9331aff43c0ab8a2ec2c608b6d7d1756dbb6cb"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e70fac33e8b4ac63dfc4c956fd7d85a0b1139adcfc0d964ce288b7c527537fea"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b33d7a286c0dc1a15f5fc864cc48ae92a846df287ceac2dd499926c3801054a6"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f6d5749fdd33d90e34c2efb174c7e236829147a2713334d708746e94c4bde40d"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22aa8f2272d81d9317ff5756bb108021a056805ce63dd3630e27d042c8092798"}, + {file = "propcache-0.2.0-cp39-cp39-win32.whl", hash = "sha256:73e4b40ea0eda421b115248d7e79b59214411109a5bc47d0d48e4c73e3b8fcf9"}, + {file = "propcache-0.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:9517d5e9e0731957468c29dbfd0f976736a0e55afaea843726e887f36fe017df"}, + {file = "propcache-0.2.0-py3-none-any.whl", hash = "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036"}, + {file = "propcache-0.2.0.tar.gz", hash = "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70"}, +] + [[package]] name = "protobuf" version = "4.24.4" @@ -2398,6 +3405,36 @@ files = [ {file = "protobuf-4.24.4.tar.gz", hash = "sha256:5a70731910cd9104762161719c3d883c960151eea077134458503723b60e3667"}, ] +[[package]] +name = "psutil" +version = "6.1.0" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "psutil-6.1.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ff34df86226c0227c52f38b919213157588a678d049688eded74c76c8ba4a5d0"}, + {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:c0e0c00aa18ca2d3b2b991643b799a15fc8f0563d2ebb6040f64ce8dc027b942"}, + {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:000d1d1ebd634b4efb383f4034437384e44a6d455260aaee2eca1e9c1b55f047"}, + {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5cd2bcdc75b452ba2e10f0e8ecc0b57b827dd5d7aaffbc6821b2a9a242823a76"}, + {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:045f00a43c737f960d273a83973b2511430d61f283a44c96bf13a6e829ba8fdc"}, + {file = "psutil-6.1.0-cp27-none-win32.whl", hash = "sha256:9118f27452b70bb1d9ab3198c1f626c2499384935aaf55388211ad982611407e"}, + {file = "psutil-6.1.0-cp27-none-win_amd64.whl", hash = "sha256:a8506f6119cff7015678e2bce904a4da21025cc70ad283a53b099e7620061d85"}, + {file = "psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688"}, + {file = "psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:498c6979f9c6637ebc3a73b3f87f9eb1ec24e1ce53a7c5173b8508981614a90b"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d905186d647b16755a800e7263d43df08b790d709d575105d419f8b6ef65423a"}, + {file = "psutil-6.1.0-cp36-cp36m-win32.whl", hash = "sha256:6d3fbbc8d23fcdcb500d2c9f94e07b1342df8ed71b948a2649b5cb060a7c94ca"}, + {file = "psutil-6.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1209036fbd0421afde505a4879dee3b2fd7b1e14fee81c0069807adcbbcca747"}, + {file = "psutil-6.1.0-cp37-abi3-win32.whl", hash = "sha256:1ad45a1f5d0b608253b11508f80940985d1d0c8f6111b5cb637533a0e6ddc13e"}, + {file = "psutil-6.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:a8fb3752b491d246034fa4d279ff076501588ce8cbcdbb62c32fd7a377d996be"}, + {file = "psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a"}, +] + +[package.extras] +dev = ["black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest-cov", "requests", "rstcheck", "ruff", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "wheel"] +test = ["pytest", "pytest-xdist", "setuptools"] + [[package]] name = "py" version = "1.11.0" @@ -2431,6 +3468,23 @@ dev = ["bumpversion (>=0.5.3,<1)", "flake8 (==3.5.0)", "mypy (==0.641)", "mypy-e lint = ["flake8 (==3.5.0)", "mypy (==0.641)", "mypy-extensions (>=0.4.1)"] test = ["pytest (==6.2.5)", "pytest-xdist (==1.26.0)"] +[[package]] +name = "py-eth-sig-utils" +version = "0.4.0" +description = "Python Ethereum Signing Utils" +optional = false +python-versions = "*" +files = [ + {file = "py_eth_sig_utils-0.4.0-py3-none-any.whl", hash = "sha256:8f2fe9b8d3c475bfca3b3bb901f6ef79e42b7b4abcd00ad5de5d483fff8359e9"}, + {file = "py_eth_sig_utils-0.4.0.tar.gz", hash = "sha256:bdf76b06677a43eba42e29ddebe6832bfbdda4c09a6b171d0de318e5a06992ad"}, +] + +[package.dependencies] +eth-abi = ">=1.1.1" +py-ecc = ">=1.7.1" +pycryptodome = ">=3.4.7" +rlp = ">=1.1.0" + [[package]] name = "py-multibase" version = "1.0.3" @@ -2463,6 +3517,32 @@ morphys = ">=1.0,<2.0" six = ">=1.10.0,<2.0" varint = ">=1.0.2,<2.0.0" +[[package]] +name = "pyalgotrade" +version = "0.20" +description = "Python Algorithmic Trading" +optional = false +python-versions = "*" +files = [ + {file = "PyAlgoTrade-0.20.tar.gz", hash = "sha256:7927c87af202869155280a93ff6ee934bb5b46cdb1f20b70f7407337f8541cbd"}, +] + +[package.dependencies] +matplotlib = "*" +numpy = "*" +python-dateutil = "*" +pytz = "*" +requests = "*" +retrying = "*" +scipy = "*" +six = "*" +tornado = "*" +tweepy = "*" +ws4py = ">=0.3.4" + +[package.extras] +talib = ["Cython", "TA-Lib"] + [[package]] name = "pycparser" version = "2.22" @@ -2476,43 +3556,82 @@ files = [ [[package]] name = "pycryptodome" -version = "3.20.0" +version = "3.18.0" description = "Cryptographic library for Python" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ - {file = "pycryptodome-3.20.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:f0e6d631bae3f231d3634f91ae4da7a960f7ff87f2865b2d2b831af1dfb04e9a"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:baee115a9ba6c5d2709a1e88ffe62b73ecc044852a925dcb67713a288c4ec70f"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:417a276aaa9cb3be91f9014e9d18d10e840a7a9b9a9be64a42f553c5b50b4d1d"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a1250b7ea809f752b68e3e6f3fd946b5939a52eaeea18c73bdab53e9ba3c2dd"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:d5954acfe9e00bc83ed9f5cb082ed22c592fbbef86dc48b907238be64ead5c33"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-win32.whl", hash = "sha256:06d6de87c19f967f03b4cf9b34e538ef46e99a337e9a61a77dbe44b2cbcf0690"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ec0bb1188c1d13426039af8ffcb4dbe3aad1d7680c35a62d8eaf2a529b5d3d4f"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5601c934c498cd267640b57569e73793cb9a83506f7c73a8ec57a516f5b0b091"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d29daa681517f4bc318cd8a23af87e1f2a7bad2fe361e8aa29c77d652a065de4"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3427d9e5310af6680678f4cce149f54e0bb4af60101c7f2c16fdf878b39ccccc"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:3cd3ef3aee1079ae44afaeee13393cf68b1058f70576b11439483e34f93cf818"}, - {file = "pycryptodome-3.20.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac1c7c0624a862f2e53438a15c9259d1655325fc2ec4392e66dc46cdae24d044"}, - {file = "pycryptodome-3.20.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:76658f0d942051d12a9bd08ca1b6b34fd762a8ee4240984f7c06ddfb55eaf15a"}, - {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f35d6cee81fa145333137009d9c8ba90951d7d77b67c79cbe5f03c7eb74d8fe2"}, - {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76cb39afede7055127e35a444c1c041d2e8d2f1f9c121ecef573757ba4cd2c3c"}, - {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a4c4dc60b78ec41d2afa392491d788c2e06edf48580fbfb0dd0f828af49d25"}, - {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fb3b87461fa35afa19c971b0a2b7456a7b1db7b4eba9a8424666104925b78128"}, - {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:acc2614e2e5346a4a4eab6e199203034924313626f9620b7b4b38e9ad74b7e0c"}, - {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:210ba1b647837bfc42dd5a813cdecb5b86193ae11a3f5d972b9a0ae2c7e9e4b4"}, - {file = "pycryptodome-3.20.0-cp35-abi3-win32.whl", hash = "sha256:8d6b98d0d83d21fb757a182d52940d028564efe8147baa9ce0f38d057104ae72"}, - {file = "pycryptodome-3.20.0-cp35-abi3-win_amd64.whl", hash = "sha256:9b3ae153c89a480a0ec402e23db8d8d84a3833b65fa4b15b81b83be9d637aab9"}, - {file = "pycryptodome-3.20.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:4401564ebf37dfde45d096974c7a159b52eeabd9969135f0426907db367a652a"}, - {file = "pycryptodome-3.20.0-pp27-pypy_73-win32.whl", hash = "sha256:ec1f93feb3bb93380ab0ebf8b859e8e5678c0f010d2d78367cf6bc30bfeb148e"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:acae12b9ede49f38eb0ef76fdec2df2e94aad85ae46ec85be3648a57f0a7db04"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f47888542a0633baff535a04726948e876bf1ed880fddb7c10a736fa99146ab3"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e0e4a987d38cfc2e71b4a1b591bae4891eeabe5fa0f56154f576e26287bfdea"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c18b381553638414b38705f07d1ef0a7cf301bc78a5f9bc17a957eb19446834b"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a60fedd2b37b4cb11ccb5d0399efe26db9e0dd149016c1cc6c8161974ceac2d6"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:405002eafad114a2f9a930f5db65feef7b53c4784495dd8758069b89baf68eab"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ab6ab0cb755154ad14e507d1df72de9897e99fd2d4922851a276ccc14f4f1a5"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:acf6e43fa75aca2d33e93409f2dafe386fe051818ee79ee8a3e21de9caa2ac9e"}, - {file = "pycryptodome-3.20.0.tar.gz", hash = "sha256:09609209ed7de61c2b560cc5c8c4fbf892f8b15b1faf7e4cbffac97db1fffda7"}, + {file = "pycryptodome-3.18.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:d1497a8cd4728db0e0da3c304856cb37c0c4e3d0b36fcbabcc1600f18504fc54"}, + {file = "pycryptodome-3.18.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:928078c530da78ff08e10eb6cada6e0dff386bf3d9fa9871b4bbc9fbc1efe024"}, + {file = "pycryptodome-3.18.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:157c9b5ba5e21b375f052ca78152dd309a09ed04703fd3721dce3ff8ecced148"}, + {file = "pycryptodome-3.18.0-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:d20082bdac9218649f6abe0b885927be25a917e29ae0502eaf2b53f1233ce0c2"}, + {file = "pycryptodome-3.18.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:e8ad74044e5f5d2456c11ed4cfd3e34b8d4898c0cb201c4038fe41458a82ea27"}, + {file = "pycryptodome-3.18.0-cp27-cp27m-win32.whl", hash = "sha256:62a1e8847fabb5213ccde38915563140a5b338f0d0a0d363f996b51e4a6165cf"}, + {file = "pycryptodome-3.18.0-cp27-cp27m-win_amd64.whl", hash = "sha256:16bfd98dbe472c263ed2821284118d899c76968db1a6665ade0c46805e6b29a4"}, + {file = "pycryptodome-3.18.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:7a3d22c8ee63de22336679e021c7f2386f7fc465477d59675caa0e5706387944"}, + {file = "pycryptodome-3.18.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:78d863476e6bad2a592645072cc489bb90320972115d8995bcfbee2f8b209918"}, + {file = "pycryptodome-3.18.0-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:b6a610f8bfe67eab980d6236fdc73bfcdae23c9ed5548192bb2d530e8a92780e"}, + {file = "pycryptodome-3.18.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:422c89fd8df8a3bee09fb8d52aaa1e996120eafa565437392b781abec2a56e14"}, + {file = "pycryptodome-3.18.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:9ad6f09f670c466aac94a40798e0e8d1ef2aa04589c29faa5b9b97566611d1d1"}, + {file = "pycryptodome-3.18.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:53aee6be8b9b6da25ccd9028caf17dcdce3604f2c7862f5167777b707fbfb6cb"}, + {file = "pycryptodome-3.18.0-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:10da29526a2a927c7d64b8f34592f461d92ae55fc97981aab5bbcde8cb465bb6"}, + {file = "pycryptodome-3.18.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f21efb8438971aa16924790e1c3dba3a33164eb4000106a55baaed522c261acf"}, + {file = "pycryptodome-3.18.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4944defabe2ace4803f99543445c27dd1edbe86d7d4edb87b256476a91e9ffa4"}, + {file = "pycryptodome-3.18.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:51eae079ddb9c5f10376b4131be9589a6554f6fd84f7f655180937f611cd99a2"}, + {file = "pycryptodome-3.18.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:83c75952dcf4a4cebaa850fa257d7a860644c70a7cd54262c237c9f2be26f76e"}, + {file = "pycryptodome-3.18.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:957b221d062d5752716923d14e0926f47670e95fead9d240fa4d4862214b9b2f"}, + {file = "pycryptodome-3.18.0-cp35-abi3-win32.whl", hash = "sha256:795bd1e4258a2c689c0b1f13ce9684fa0dd4c0e08680dcf597cf9516ed6bc0f3"}, + {file = "pycryptodome-3.18.0-cp35-abi3-win_amd64.whl", hash = "sha256:b1d9701d10303eec8d0bd33fa54d44e67b8be74ab449052a8372f12a66f93fb9"}, + {file = "pycryptodome-3.18.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:cb1be4d5af7f355e7d41d36d8eec156ef1382a88638e8032215c215b82a4b8ec"}, + {file = "pycryptodome-3.18.0-pp27-pypy_73-win32.whl", hash = "sha256:fc0a73f4db1e31d4a6d71b672a48f3af458f548059aa05e83022d5f61aac9c08"}, + {file = "pycryptodome-3.18.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f022a4fd2a5263a5c483a2bb165f9cb27f2be06f2f477113783efe3fe2ad887b"}, + {file = "pycryptodome-3.18.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:363dd6f21f848301c2dcdeb3c8ae5f0dee2286a5e952a0f04954b82076f23825"}, + {file = "pycryptodome-3.18.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12600268763e6fec3cefe4c2dcdf79bde08d0b6dc1813887e789e495cb9f3403"}, + {file = "pycryptodome-3.18.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4604816adebd4faf8810782f137f8426bf45fee97d8427fa8e1e49ea78a52e2c"}, + {file = "pycryptodome-3.18.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:01489bbdf709d993f3058e2996f8f40fee3f0ea4d995002e5968965fa2fe89fb"}, + {file = "pycryptodome-3.18.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3811e31e1ac3069988f7a1c9ee7331b942e605dfc0f27330a9ea5997e965efb2"}, + {file = "pycryptodome-3.18.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4b967bb11baea9128ec88c3d02f55a3e338361f5e4934f5240afcb667fdaec"}, + {file = "pycryptodome-3.18.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:9c8eda4f260072f7dbe42f473906c659dcbadd5ae6159dfb49af4da1293ae380"}, + {file = "pycryptodome-3.18.0.tar.gz", hash = "sha256:c9adee653fc882d98956e33ca2c1fb582e23a8af7ac82fee75bd6113c55a0413"}, +] + +[[package]] +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pyheck" +version = "0.1.5" +description = "Python bindings for heck, the Rust case conversion library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyheck-0.1.5-cp37-abi3-macosx_10_7_x86_64.whl", hash = "sha256:44caf2b7a49d71fdeb0469e9f35886987ad815a8638b3c5b5c83f351d6aed413"}, + {file = "pyheck-0.1.5-cp37-abi3-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:316a842b94beff6e59a97dbcc590e9be92a932e59126b0faa9ac750384f27eaf"}, + {file = "pyheck-0.1.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b6169397395ff041f056bfb36c1957a788a1cd7cb967a927fcae7917ff1b6aa"}, + {file = "pyheck-0.1.5-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e9d101e1c599227280e34eeccab0414246e70a91a1cabb4c4868dca284f2be7d"}, + {file = "pyheck-0.1.5-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:69d6509138909df92b2f2f837518dca118ef08ae3c804044ae511b81b7aecb4d"}, + {file = "pyheck-0.1.5-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ce4a2e1b4778051b8f31183e321a034603f3957b6e95cf03bf5f231c8ea3066"}, + {file = "pyheck-0.1.5-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69387b70d910637ab6dc8dc378c8e0b4037cee2c51a9c6f64ce5331b010f5de3"}, + {file = "pyheck-0.1.5-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0fb50b7d899d2a583ec2ac291b8ec2afb10f0e32c4ac290148d3da15927787f8"}, + {file = "pyheck-0.1.5-cp37-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:aa8dfd0883212f8495e0bae6eb6ea670c56f9b197b5fe6fb5cae9fd5ec56fb7c"}, + {file = "pyheck-0.1.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b7c07506b9591e27f8241bf7a72bc4d5c4ac30dedb332efb87e402e49029f233"}, + {file = "pyheck-0.1.5-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:9ee256cafbdab6c5fcca22d0910176d820bf1e1298773e64f4eea79f51218cc7"}, + {file = "pyheck-0.1.5-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:e9ba36060abc55127c3813de398b4013c05be6118cfae3cfa3d978f7b4c84dea"}, + {file = "pyheck-0.1.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:64201a6d213bec443aeb33f66c60cea61aaf6257e48a19159ac69a5afad4768e"}, + {file = "pyheck-0.1.5-cp37-abi3-win32.whl", hash = "sha256:1501fcfd15f7c05c6bfe38915f5e514ac95fc63e945f7d8b089d30c1b8fdb2c5"}, + {file = "pyheck-0.1.5-cp37-abi3-win_amd64.whl", hash = "sha256:e519f80a0ef87a8f880bfdf239e396e238dcaed34bec1ea7ef526c4873220e82"}, + {file = "pyheck-0.1.5.tar.gz", hash = "sha256:5c9fe372d540c5dbcb76bf062f951d998d0e14c906c842a52f1cd5de208e183a"}, ] [[package]] @@ -2556,6 +3675,20 @@ cffi = ">=1.4.1" docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] +[[package]] +name = "pyparsing" +version = "3.2.0" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pyparsing-3.2.0-py3-none-any.whl", hash = "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84"}, + {file = "pyparsing-3.2.0.tar.gz", hash = "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + [[package]] name = "pyrsistent" version = "0.20.0" @@ -2685,6 +3818,21 @@ files = [ packaging = ">=17.1" pytest = ">=5.3" +[[package]] +name = "pytest-xprocess" +version = "0.18.1" +description = "A pytest plugin for managing processes across test runs." +optional = false +python-versions = ">=3.5" +files = [ + {file = "pytest-xprocess-0.18.1.tar.gz", hash = "sha256:fd9f30ed1584b5833bc34494748adf0fb9de3ca7bacc4e88ad71989c21cba266"}, + {file = "pytest_xprocess-0.18.1-py3-none-any.whl", hash = "sha256:6f2aba817d842518d9d9dfb7e9adfe2a6e354a4359f4166bef0822ef4be1c9db"}, +] + +[package.dependencies] +psutil = "*" +pytest = ">=2.8" + [[package]] name = "python-baseconv" version = "1.2.2" @@ -2711,13 +3859,13 @@ six = ">=1.5" [[package]] name = "python-dotenv" -version = "0.21.1" +version = "0.17.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false -python-versions = ">=3.7" +python-versions = "*" files = [ - {file = "python-dotenv-0.21.1.tar.gz", hash = "sha256:1c93de8f636cde3ce377292818d0e440b6e45a82f215c3744979151fa8151c49"}, - {file = "python_dotenv-0.21.1-py3-none-any.whl", hash = "sha256:41e12e0318bebc859fcc4d97d4db8d20ad21721a6aa5047dd59f090391cb549a"}, + {file = "python-dotenv-0.17.1.tar.gz", hash = "sha256:b1ae5e9643d5ed987fc57cc2583021e38db531946518130777734f9589b3141f"}, + {file = "python_dotenv-0.17.1-py2.py3-none-any.whl", hash = "sha256:00aa34e92d992e9f8383730816359647f358f4a3be1ba45e5a5cefd27ee91544"}, ] [package.extras] @@ -2747,25 +3895,29 @@ files = [ [[package]] name = "pywin32" -version = "306" +version = "308" description = "Python for Window Extensions" optional = false python-versions = "*" files = [ - {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, - {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, - {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, - {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, - {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, - {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, - {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, - {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, - {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, - {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, - {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, - {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, - {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, - {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, + {file = "pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e"}, + {file = "pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e"}, + {file = "pywin32-308-cp310-cp310-win_arm64.whl", hash = "sha256:a5ab5381813b40f264fa3495b98af850098f814a25a63589a8e9eb12560f450c"}, + {file = "pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a"}, + {file = "pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b"}, + {file = "pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6"}, + {file = "pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897"}, + {file = "pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47"}, + {file = "pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091"}, + {file = "pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed"}, + {file = "pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4"}, + {file = "pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd"}, + {file = "pywin32-308-cp37-cp37m-win32.whl", hash = "sha256:1f696ab352a2ddd63bd07430080dd598e6369152ea13a25ebcdd2f503a38f1ff"}, + {file = "pywin32-308-cp37-cp37m-win_amd64.whl", hash = "sha256:13dcb914ed4347019fbec6697a01a0aec61019c1046c2b905410d197856326a6"}, + {file = "pywin32-308-cp38-cp38-win32.whl", hash = "sha256:5794e764ebcabf4ff08c555b31bd348c9025929371763b2183172ff4708152f0"}, + {file = "pywin32-308-cp38-cp38-win_amd64.whl", hash = "sha256:3b92622e29d651c6b783e368ba7d6722b1634b8e70bd376fd7610fe1992e19de"}, + {file = "pywin32-308-cp39-cp39-win32.whl", hash = "sha256:7873ca4dc60ab3287919881a7d4f88baee4a6e639aa6962de25a98ba6b193341"}, + {file = "pywin32-308-cp39-cp39-win_amd64.whl", hash = "sha256:71b3322d949b4cc20776436a9c9ba0eeedcbc9c650daa536df63f0ff111bb920"}, ] [[package]] @@ -2952,6 +4104,24 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "requests-oauthlib" +version = "1.3.1" +description = "OAuthlib authentication support for Requests." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"}, + {file = "requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5"}, +] + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] + [[package]] name = "requests-toolbelt" version = "1.0.0" @@ -2966,6 +4136,76 @@ files = [ [package.dependencies] requests = ">=2.0.1,<3.0.0" +[[package]] +name = "retrying" +version = "1.3.4" +description = "Retrying" +optional = false +python-versions = "*" +files = [ + {file = "retrying-1.3.4-py3-none-any.whl", hash = "sha256:8cc4d43cb8e1125e0ff3344e9de678fefd85db3b750b81b2240dc0183af37b35"}, + {file = "retrying-1.3.4.tar.gz", hash = "sha256:345da8c5765bd982b1d1915deb9102fd3d1f7ad16bd84a9700b85f64d24e8f3e"}, +] + +[package.dependencies] +six = ">=1.7.0" + +[[package]] +name = "rfc3986" +version = "1.5.0" +description = "Validating URI References per RFC 3986" +optional = false +python-versions = "*" +files = [ + {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, + {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, +] + +[package.dependencies] +idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} + +[package.extras] +idna2008 = ["idna"] + +[[package]] +name = "rich" +version = "13.9.2" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "rich-13.9.2-py3-none-any.whl", hash = "sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1"}, + {file = "rich-13.9.2.tar.gz", hash = "sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "rich-click" +version = "1.8.3" +description = "Format click help output nicely with rich" +optional = false +python-versions = ">=3.7" +files = [ + {file = "rich_click-1.8.3-py3-none-any.whl", hash = "sha256:636d9c040d31c5eee242201b5bf4f2d358bfae4db14bb22ec1cafa717cfd02cd"}, + {file = "rich_click-1.8.3.tar.gz", hash = "sha256:6d75bdfa7aa9ed2c467789a0688bc6da23fbe3a143e19aa6ad3f8bac113d2ab3"}, +] + +[package.dependencies] +click = ">=7" +rich = ">=10.7" +typing-extensions = "*" + +[package.extras] +dev = ["mypy", "packaging", "pre-commit", "pytest", "pytest-cov", "rich-codex", "ruff", "types-setuptools"] +docs = ["markdown-include", "mkdocs", "mkdocs-glightbox", "mkdocs-material-extensions", "mkdocs-material[imaging] (>=9.5.18,<9.6.0)", "mkdocs-rss-plugin", "mkdocstrings[python]", "rich-codex"] + [[package]] name = "rlp" version = "3.0.0" @@ -2987,6 +4227,56 @@ lint = ["flake8 (==3.4.1)"] rust-backend = ["rusty-rlp (>=0.2.1,<0.3)"] test = ["hypothesis (==5.19.0)", "pytest (>=6.2.5,<7)", "tox (>=2.9.1,<3)"] +[[package]] +name = "scipy" +version = "1.14.1" +description = "Fundamental algorithms for scientific computing in Python" +optional = false +python-versions = ">=3.10" +files = [ + {file = "scipy-1.14.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:b28d2ca4add7ac16ae8bb6632a3c86e4b9e4d52d3e34267f6e1b0c1f8d87e389"}, + {file = "scipy-1.14.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d0d2821003174de06b69e58cef2316a6622b60ee613121199cb2852a873f8cf3"}, + {file = "scipy-1.14.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8bddf15838ba768bb5f5083c1ea012d64c9a444e16192762bd858f1e126196d0"}, + {file = "scipy-1.14.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:97c5dddd5932bd2a1a31c927ba5e1463a53b87ca96b5c9bdf5dfd6096e27efc3"}, + {file = "scipy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ff0a7e01e422c15739ecd64432743cf7aae2b03f3084288f399affcefe5222d"}, + {file = "scipy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e32dced201274bf96899e6491d9ba3e9a5f6b336708656466ad0522d8528f69"}, + {file = "scipy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8426251ad1e4ad903a4514712d2fa8fdd5382c978010d1c6f5f37ef286a713ad"}, + {file = "scipy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:a49f6ed96f83966f576b33a44257d869756df6cf1ef4934f59dd58b25e0327e5"}, + {file = "scipy-1.14.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:2da0469a4ef0ecd3693761acbdc20f2fdeafb69e6819cc081308cc978153c675"}, + {file = "scipy-1.14.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c0ee987efa6737242745f347835da2cc5bb9f1b42996a4d97d5c7ff7928cb6f2"}, + {file = "scipy-1.14.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3a1b111fac6baec1c1d92f27e76511c9e7218f1695d61b59e05e0fe04dc59617"}, + {file = "scipy-1.14.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8475230e55549ab3f207bff11ebfc91c805dc3463ef62eda3ccf593254524ce8"}, + {file = "scipy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:278266012eb69f4a720827bdd2dc54b2271c97d84255b2faaa8f161a158c3b37"}, + {file = "scipy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fef8c87f8abfb884dac04e97824b61299880c43f4ce675dd2cbeadd3c9b466d2"}, + {file = "scipy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b05d43735bb2f07d689f56f7b474788a13ed8adc484a85aa65c0fd931cf9ccd2"}, + {file = "scipy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:716e389b694c4bb564b4fc0c51bc84d381735e0d39d3f26ec1af2556ec6aad94"}, + {file = "scipy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:631f07b3734d34aced009aaf6fedfd0eb3498a97e581c3b1e5f14a04164a456d"}, + {file = "scipy-1.14.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:af29a935803cc707ab2ed7791c44288a682f9c8107bc00f0eccc4f92c08d6e07"}, + {file = "scipy-1.14.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2843f2d527d9eebec9a43e6b406fb7266f3af25a751aa91d62ff416f54170bc5"}, + {file = "scipy-1.14.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:eb58ca0abd96911932f688528977858681a59d61a7ce908ffd355957f7025cfc"}, + {file = "scipy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30ac8812c1d2aab7131a79ba62933a2a76f582d5dbbc695192453dae67ad6310"}, + {file = "scipy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f9ea80f2e65bdaa0b7627fb00cbeb2daf163caa015e59b7516395fe3bd1e066"}, + {file = "scipy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:edaf02b82cd7639db00dbff629995ef185c8df4c3ffa71a5562a595765a06ce1"}, + {file = "scipy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2ff38e22128e6c03ff73b6bb0f85f897d2362f8c052e3b8ad00532198fbdae3f"}, + {file = "scipy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1729560c906963fc8389f6aac023739ff3983e727b1a4d87696b7bf108316a79"}, + {file = "scipy-1.14.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:4079b90df244709e675cdc8b93bfd8a395d59af40b72e339c2287c91860deb8e"}, + {file = "scipy-1.14.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e0cf28db0f24a38b2a0ca33a85a54852586e43cf6fd876365c86e0657cfe7d73"}, + {file = "scipy-1.14.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0c2f95de3b04e26f5f3ad5bb05e74ba7f68b837133a4492414b3afd79dfe540e"}, + {file = "scipy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b99722ea48b7ea25e8e015e8341ae74624f72e5f21fc2abd45f3a93266de4c5d"}, + {file = "scipy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5149e3fd2d686e42144a093b206aef01932a0059c2a33ddfa67f5f035bdfe13e"}, + {file = "scipy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4f5a7c49323533f9103d4dacf4e4f07078f360743dec7f7596949149efeec06"}, + {file = "scipy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:baff393942b550823bfce952bb62270ee17504d02a1801d7fd0719534dfb9c84"}, + {file = "scipy-1.14.1.tar.gz", hash = "sha256:5a275584e726026a5699459aa72f828a610821006228e841b94275c4a7c08417"}, +] + +[package.dependencies] +numpy = ">=1.23.5,<2.3" + +[package.extras] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] +doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.13.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<=7.3.7)", "sphinx-design (>=0.4.0)"] +test = ["Cython", "array-api-strict (>=2.0)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + [[package]] name = "semver" version = "2.13.0" @@ -3000,23 +4290,19 @@ files = [ [[package]] name = "setuptools" -version = "75.1.0" +version = "69.5.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-75.1.0-py3-none-any.whl", hash = "sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2"}, - {file = "setuptools-75.1.0.tar.gz", hash = "sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538"}, + {file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"}, + {file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" @@ -3040,6 +4326,48 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +[[package]] +name = "solana" +version = "0.30.2" +description = "Solana Python API" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "solana-0.30.2-py3-none-any.whl", hash = "sha256:d7e8295a1f86982ba51e78a65c16ce55f4a9e9caa8938564922a209ddfb2a01f"}, + {file = "solana-0.30.2.tar.gz", hash = "sha256:7b16e76cdd1d3024219679cdb73c20324d6d79e3c9766fe0ca52be79ef5ff691"}, +] + +[package.dependencies] +cachetools = ">=4.2.2,<5.0.0" +construct-typing = ">=0.5.2,<0.6.0" +httpx = ">=0.23.0,<0.24.0" +solders = ">=0.18.0,<0.19.0" +types-cachetools = ">=4.2.4,<5.0.0" +typing-extensions = ">=4.2.0" +websockets = ">=9.0,<12.0" + +[[package]] +name = "solders" +version = "0.18.1" +description = "Python bindings for Solana Rust tools" +optional = false +python-versions = ">=3.7" +files = [ + {file = "solders-0.18.1-cp37-abi3-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:1b20230838626fad26d5bdaf8ebe3db3b660ef9f56cc271feca8970d464ea11f"}, + {file = "solders-0.18.1-cp37-abi3-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3d2503693d0fb0efd37e3f921277327ff664bd04fff551346fad565dd8b9185a"}, + {file = "solders-0.18.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ee08cd1de84463b83551810981c25dcca4aa42ab57b8ba823d62dbab9d202"}, + {file = "solders-0.18.1-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b78482deeed583b473e70314336d26562b5f2f14fa777a83b9406835eb3e988"}, + {file = "solders-0.18.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3da01fcfcfd2154ff41328a3b7a32db655f8aace4ca146a7bf779c6088866daf"}, + {file = "solders-0.18.1-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:0b6bd026bdfce27daacefa405f0e46f44f9e8dd7e60fd36d23278b08a0f40482"}, + {file = "solders-0.18.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:02788caa3b82e846944a37f86dc673b6c9d80108679f44109f6c7acfd98808ec"}, + {file = "solders-0.18.1-cp37-abi3-win_amd64.whl", hash = "sha256:1587231b57a94e29df3945fa2582817cdb1c935c7cc3b446140ae4154912d1e6"}, + {file = "solders-0.18.1.tar.gz", hash = "sha256:1b41c36e331e2323ed4be1253fda2221391956b4a528270acfbf921e2a3f0329"}, +] + +[package.dependencies] +jsonalias = "0.1.1" +typing-extensions = ">=4.2.0" + [[package]] name = "sortedcontainers" version = "2.4.0" @@ -3051,6 +4379,20 @@ files = [ {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, ] +[[package]] +name = "sumtypes" +version = "0.1a6" +description = "Algebraic types for Python (notably providing Sum Types, aka Tagged Unions)" +optional = false +python-versions = "*" +files = [ + {file = "sumtypes-0.1a6-py2.py3-none-any.whl", hash = "sha256:3e9d71322dd927d25d935072f8be7daec655ea292fd392359a5bb2c1e53dfdc3"}, + {file = "sumtypes-0.1a6.tar.gz", hash = "sha256:1a6ff095e06a1885f340ddab803e0f38e3f9bed81f9090164ca9682e04e96b43"}, +] + +[package.dependencies] +attrs = "*" + [[package]] name = "texttable" version = "1.6.7" @@ -3075,13 +4417,13 @@ files = [ [[package]] name = "tomli" -version = "2.0.1" +version = "2.0.2" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, + {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, ] [[package]] @@ -3123,13 +4465,33 @@ vulture = ["vulture (==2.7)"] [[package]] name = "toolz" -version = "0.12.1" +version = "0.11.2" description = "List processing tools and functional utilities" optional = false -python-versions = ">=3.7" +python-versions = ">=3.5" +files = [ + {file = "toolz-0.11.2-py3-none-any.whl", hash = "sha256:a5700ce83414c64514d82d60bcda8aabfde092d1c1a8663f9200c07fdcc6da8f"}, + {file = "toolz-0.11.2.tar.gz", hash = "sha256:6b312d5e15138552f1bda8a4e66c30e236c831b612b2bf0005f8a1df10a4bc33"}, +] + +[[package]] +name = "tornado" +version = "6.4.1" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +optional = false +python-versions = ">=3.8" files = [ - {file = "toolz-0.12.1-py3-none-any.whl", hash = "sha256:d22731364c07d72eea0a0ad45bafb2c2937ab6fd38a3507bf55eae8744aa7d85"}, - {file = "toolz-0.12.1.tar.gz", hash = "sha256:ecca342664893f177a13dac0e6b41cbd8ac25a358e5f215316d43e2100224f4d"}, + {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8"}, + {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698"}, + {file = "tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d"}, + {file = "tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7"}, + {file = "tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9"}, ] [[package]] @@ -3157,6 +4519,40 @@ virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2, docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"] +[[package]] +name = "tweepy" +version = "4.14.0" +description = "Twitter library for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tweepy-4.14.0-py3-none-any.whl", hash = "sha256:db6d3844ccc0c6d27f339f12ba8acc89912a961da513c1ae50fa2be502a56afb"}, + {file = "tweepy-4.14.0.tar.gz", hash = "sha256:1f9f1707d6972de6cff6c5fd90dfe6a449cd2e0d70bd40043ffab01e07a06c8c"}, +] + +[package.dependencies] +oauthlib = ">=3.2.0,<4" +requests = ">=2.27.0,<3" +requests-oauthlib = ">=1.2.0,<2" + +[package.extras] +async = ["aiohttp (>=3.7.3,<4)", "async-lru (>=1.0.3,<3)"] +dev = ["coverage (>=4.4.2)", "coveralls (>=2.1.0)", "tox (>=3.21.0)"] +docs = ["myst-parser (==0.15.2)", "readthedocs-sphinx-search (==0.1.1)", "sphinx (==4.2.0)", "sphinx-hoverxref (==0.7b1)", "sphinx-rtd-theme (==1.0.0)", "sphinx-tabs (==3.2.0)"] +socks = ["requests[socks] (>=2.27.0,<3)"] +test = ["vcrpy (>=1.10.3)"] + +[[package]] +name = "types-cachetools" +version = "4.2.10" +description = "Typing stubs for cachetools" +optional = false +python-versions = "*" +files = [ + {file = "types-cachetools-4.2.10.tar.gz", hash = "sha256:b1cb18aaff25d2ad47a060413c660c39fadddb01f72012dd1134584b1fdaada5"}, + {file = "types_cachetools-4.2.10-py3-none-any.whl", hash = "sha256:48301115189d4879d0960baac5a8a2b2d31ce6129b2ce3b915000ed337284898"}, +] + [[package]] name = "typing-extensions" version = "4.12.2" @@ -3168,22 +4564,49 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +[[package]] +name = "tzdata" +version = "2024.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, + {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, +] + +[[package]] +name = "tzlocal" +version = "5.2" +description = "tzinfo object for the local timezone" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"}, + {file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"}, +] + +[package.dependencies] +tzdata = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] + [[package]] name = "urllib3" -version = "2.2.3" +version = "1.26.16" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.8" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ - {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, - {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, + {file = "urllib3-1.26.16-py2.py3-none-any.whl", hash = "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f"}, + {file = "urllib3-1.26.16.tar.gz", hash = "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "valory-docker-compose" @@ -3225,13 +4648,13 @@ files = [ [[package]] name = "virtualenv" -version = "20.26.5" +version = "20.27.0" description = "Virtual Python Environment builder" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "virtualenv-20.26.5-py3-none-any.whl", hash = "sha256:4f3ac17b81fba3ce3bd6f4ead2749a72da5929c01774948e243db9ba41df4ff6"}, - {file = "virtualenv-20.26.5.tar.gz", hash = "sha256:ce489cac131aa58f4b25e321d6d186171f78e6cb13fafbf32a840cee67733ff4"}, + {file = "virtualenv-20.27.0-py3-none-any.whl", hash = "sha256:44a72c29cceb0ee08f300b314848c86e57bf8d1f13107a5e671fb9274138d655"}, + {file = "virtualenv-20.27.0.tar.gz", hash = "sha256:2ca56a68ed615b8fe4326d11a0dca5dfbe8fd68510fb6c6349163bed3c15f2b2"}, ] [package.dependencies] @@ -3245,41 +4668,41 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [[package]] name = "watchdog" -version = "5.0.2" +version = "5.0.3" description = "Filesystem events monitoring" optional = false python-versions = ">=3.9" files = [ - {file = "watchdog-5.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d961f4123bb3c447d9fcdcb67e1530c366f10ab3a0c7d1c0c9943050936d4877"}, - {file = "watchdog-5.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72990192cb63872c47d5e5fefe230a401b87fd59d257ee577d61c9e5564c62e5"}, - {file = "watchdog-5.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6bec703ad90b35a848e05e1b40bf0050da7ca28ead7ac4be724ae5ac2653a1a0"}, - {file = "watchdog-5.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dae7a1879918f6544201d33666909b040a46421054a50e0f773e0d870ed7438d"}, - {file = "watchdog-5.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c4a440f725f3b99133de610bfec93d570b13826f89616377715b9cd60424db6e"}, - {file = "watchdog-5.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8b2918c19e0d48f5f20df458c84692e2a054f02d9df25e6c3c930063eca64c1"}, - {file = "watchdog-5.0.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:aa9cd6e24126d4afb3752a3e70fce39f92d0e1a58a236ddf6ee823ff7dba28ee"}, - {file = "watchdog-5.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f627c5bf5759fdd90195b0c0431f99cff4867d212a67b384442c51136a098ed7"}, - {file = "watchdog-5.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d7594a6d32cda2b49df3fd9abf9b37c8d2f3eab5df45c24056b4a671ac661619"}, - {file = "watchdog-5.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba32efcccfe2c58f4d01115440d1672b4eb26cdd6fc5b5818f1fb41f7c3e1889"}, - {file = "watchdog-5.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:963f7c4c91e3f51c998eeff1b3fb24a52a8a34da4f956e470f4b068bb47b78ee"}, - {file = "watchdog-5.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8c47150aa12f775e22efff1eee9f0f6beee542a7aa1a985c271b1997d340184f"}, - {file = "watchdog-5.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:14dd4ed023d79d1f670aa659f449bcd2733c33a35c8ffd88689d9d243885198b"}, - {file = "watchdog-5.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b84bff0391ad4abe25c2740c7aec0e3de316fdf7764007f41e248422a7760a7f"}, - {file = "watchdog-5.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e8d5ff39f0a9968952cce548e8e08f849141a4fcc1290b1c17c032ba697b9d7"}, - {file = "watchdog-5.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fb223456db6e5f7bd9bbd5cd969f05aae82ae21acc00643b60d81c770abd402b"}, - {file = "watchdog-5.0.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9814adb768c23727a27792c77812cf4e2fd9853cd280eafa2bcfa62a99e8bd6e"}, - {file = "watchdog-5.0.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:901ee48c23f70193d1a7bc2d9ee297df66081dd5f46f0ca011be4f70dec80dab"}, - {file = "watchdog-5.0.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:638bcca3d5b1885c6ec47be67bf712b00a9ab3d4b22ec0881f4889ad870bc7e8"}, - {file = "watchdog-5.0.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:5597c051587f8757798216f2485e85eac583c3b343e9aa09127a3a6f82c65ee8"}, - {file = "watchdog-5.0.2-py3-none-manylinux2014_armv7l.whl", hash = "sha256:53ed1bf71fcb8475dd0ef4912ab139c294c87b903724b6f4a8bd98e026862e6d"}, - {file = "watchdog-5.0.2-py3-none-manylinux2014_i686.whl", hash = "sha256:29e4a2607bd407d9552c502d38b45a05ec26a8e40cc7e94db9bb48f861fa5abc"}, - {file = "watchdog-5.0.2-py3-none-manylinux2014_ppc64.whl", hash = "sha256:b6dc8f1d770a8280997e4beae7b9a75a33b268c59e033e72c8a10990097e5fde"}, - {file = "watchdog-5.0.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:d2ab34adc9bf1489452965cdb16a924e97d4452fcf88a50b21859068b50b5c3b"}, - {file = "watchdog-5.0.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:7d1aa7e4bb0f0c65a1a91ba37c10e19dabf7eaaa282c5787e51371f090748f4b"}, - {file = "watchdog-5.0.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:726eef8f8c634ac6584f86c9c53353a010d9f311f6c15a034f3800a7a891d941"}, - {file = "watchdog-5.0.2-py3-none-win32.whl", hash = "sha256:bda40c57115684d0216556671875e008279dea2dc00fcd3dde126ac8e0d7a2fb"}, - {file = "watchdog-5.0.2-py3-none-win_amd64.whl", hash = "sha256:d010be060c996db725fbce7e3ef14687cdcc76f4ca0e4339a68cc4532c382a73"}, - {file = "watchdog-5.0.2-py3-none-win_ia64.whl", hash = "sha256:3960136b2b619510569b90f0cd96408591d6c251a75c97690f4553ca88889769"}, - {file = "watchdog-5.0.2.tar.gz", hash = "sha256:dcebf7e475001d2cdeb020be630dc5b687e9acdd60d16fea6bb4508e7b94cf76"}, + {file = "watchdog-5.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:85527b882f3facda0579bce9d743ff7f10c3e1e0db0a0d0e28170a7d0e5ce2ea"}, + {file = "watchdog-5.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:53adf73dcdc0ef04f7735066b4a57a4cd3e49ef135daae41d77395f0b5b692cb"}, + {file = "watchdog-5.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e25adddab85f674acac303cf1f5835951345a56c5f7f582987d266679979c75b"}, + {file = "watchdog-5.0.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f01f4a3565a387080dc49bdd1fefe4ecc77f894991b88ef927edbfa45eb10818"}, + {file = "watchdog-5.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91b522adc25614cdeaf91f7897800b82c13b4b8ac68a42ca959f992f6990c490"}, + {file = "watchdog-5.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d52db5beb5e476e6853da2e2d24dbbbed6797b449c8bf7ea118a4ee0d2c9040e"}, + {file = "watchdog-5.0.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:94d11b07c64f63f49876e0ab8042ae034674c8653bfcdaa8c4b32e71cfff87e8"}, + {file = "watchdog-5.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:349c9488e1d85d0a58e8cb14222d2c51cbc801ce11ac3936ab4c3af986536926"}, + {file = "watchdog-5.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:53a3f10b62c2d569e260f96e8d966463dec1a50fa4f1b22aec69e3f91025060e"}, + {file = "watchdog-5.0.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:950f531ec6e03696a2414b6308f5c6ff9dab7821a768c9d5788b1314e9a46ca7"}, + {file = "watchdog-5.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae6deb336cba5d71476caa029ceb6e88047fc1dc74b62b7c4012639c0b563906"}, + {file = "watchdog-5.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1021223c08ba8d2d38d71ec1704496471ffd7be42cfb26b87cd5059323a389a1"}, + {file = "watchdog-5.0.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:752fb40efc7cc8d88ebc332b8f4bcbe2b5cc7e881bccfeb8e25054c00c994ee3"}, + {file = "watchdog-5.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a2e8f3f955d68471fa37b0e3add18500790d129cc7efe89971b8a4cc6fdeb0b2"}, + {file = "watchdog-5.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b8ca4d854adcf480bdfd80f46fdd6fb49f91dd020ae11c89b3a79e19454ec627"}, + {file = "watchdog-5.0.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:90a67d7857adb1d985aca232cc9905dd5bc4803ed85cfcdcfcf707e52049eda7"}, + {file = "watchdog-5.0.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:720ef9d3a4f9ca575a780af283c8fd3a0674b307651c1976714745090da5a9e8"}, + {file = "watchdog-5.0.3-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:223160bb359281bb8e31c8f1068bf71a6b16a8ad3d9524ca6f523ac666bb6a1e"}, + {file = "watchdog-5.0.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:560135542c91eaa74247a2e8430cf83c4342b29e8ad4f520ae14f0c8a19cfb5b"}, + {file = "watchdog-5.0.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:dd021efa85970bd4824acacbb922066159d0f9e546389a4743d56919b6758b91"}, + {file = "watchdog-5.0.3-py3-none-manylinux2014_armv7l.whl", hash = "sha256:78864cc8f23dbee55be34cc1494632a7ba30263951b5b2e8fc8286b95845f82c"}, + {file = "watchdog-5.0.3-py3-none-manylinux2014_i686.whl", hash = "sha256:1e9679245e3ea6498494b3028b90c7b25dbb2abe65c7d07423ecfc2d6218ff7c"}, + {file = "watchdog-5.0.3-py3-none-manylinux2014_ppc64.whl", hash = "sha256:9413384f26b5d050b6978e6fcd0c1e7f0539be7a4f1a885061473c5deaa57221"}, + {file = "watchdog-5.0.3-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:294b7a598974b8e2c6123d19ef15de9abcd282b0fbbdbc4d23dfa812959a9e05"}, + {file = "watchdog-5.0.3-py3-none-manylinux2014_s390x.whl", hash = "sha256:26dd201857d702bdf9d78c273cafcab5871dd29343748524695cecffa44a8d97"}, + {file = "watchdog-5.0.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:0f9332243355643d567697c3e3fa07330a1d1abf981611654a1f2bf2175612b7"}, + {file = "watchdog-5.0.3-py3-none-win32.whl", hash = "sha256:c66f80ee5b602a9c7ab66e3c9f36026590a0902db3aea414d59a2f55188c1f49"}, + {file = "watchdog-5.0.3-py3-none-win_amd64.whl", hash = "sha256:f00b4cf737f568be9665563347a910f8bdc76f88c2970121c86243c8cfdf90e9"}, + {file = "watchdog-5.0.3-py3-none-win_ia64.whl", hash = "sha256:49f4d36cb315c25ea0d946e018c01bb028048023b9e103d3d3943f58e109dd45"}, + {file = "watchdog-5.0.3.tar.gz", hash = "sha256:108f42a7f0345042a854d4d0ad0834b741d421330d5f575b81cb27b883500176"}, ] [package.extras] @@ -3336,97 +4759,80 @@ six = "*" [[package]] name = "websockets" -version = "13.1" +version = "10.4" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "websockets-13.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f48c749857f8fb598fb890a75f540e3221d0976ed0bf879cf3c7eef34151acee"}, - {file = "websockets-13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7e72ce6bda6fb9409cc1e8164dd41d7c91466fb599eb047cfda72fe758a34a7"}, - {file = "websockets-13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f779498eeec470295a2b1a5d97aa1bc9814ecd25e1eb637bd9d1c73a327387f6"}, - {file = "websockets-13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676df3fe46956fbb0437d8800cd5f2b6d41143b6e7e842e60554398432cf29b"}, - {file = "websockets-13.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7affedeb43a70351bb811dadf49493c9cfd1ed94c9c70095fd177e9cc1541fa"}, - {file = "websockets-13.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1971e62d2caa443e57588e1d82d15f663b29ff9dfe7446d9964a4b6f12c1e700"}, - {file = "websockets-13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5f2e75431f8dc4a47f31565a6e1355fb4f2ecaa99d6b89737527ea917066e26c"}, - {file = "websockets-13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58cf7e75dbf7e566088b07e36ea2e3e2bd5676e22216e4cad108d4df4a7402a0"}, - {file = "websockets-13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c90d6dec6be2c7d03378a574de87af9b1efea77d0c52a8301dd831ece938452f"}, - {file = "websockets-13.1-cp310-cp310-win32.whl", hash = "sha256:730f42125ccb14602f455155084f978bd9e8e57e89b569b4d7f0f0c17a448ffe"}, - {file = "websockets-13.1-cp310-cp310-win_amd64.whl", hash = "sha256:5993260f483d05a9737073be197371940c01b257cc45ae3f1d5d7adb371b266a"}, - {file = "websockets-13.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:61fc0dfcda609cda0fc9fe7977694c0c59cf9d749fbb17f4e9483929e3c48a19"}, - {file = "websockets-13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ceec59f59d092c5007e815def4ebb80c2de330e9588e101cf8bd94c143ec78a5"}, - {file = "websockets-13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1dca61c6db1166c48b95198c0b7d9c990b30c756fc2923cc66f68d17dc558fd"}, - {file = "websockets-13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308e20f22c2c77f3f39caca508e765f8725020b84aa963474e18c59accbf4c02"}, - {file = "websockets-13.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d516c325e6540e8a57b94abefc3459d7dab8ce52ac75c96cad5549e187e3a7"}, - {file = "websockets-13.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c6e35319b46b99e168eb98472d6c7d8634ee37750d7693656dc766395df096"}, - {file = "websockets-13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f9fee94ebafbc3117c30be1844ed01a3b177bb6e39088bc6b2fa1dc15572084"}, - {file = "websockets-13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7c1e90228c2f5cdde263253fa5db63e6653f1c00e7ec64108065a0b9713fa1b3"}, - {file = "websockets-13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6548f29b0e401eea2b967b2fdc1c7c7b5ebb3eeb470ed23a54cd45ef078a0db9"}, - {file = "websockets-13.1-cp311-cp311-win32.whl", hash = "sha256:c11d4d16e133f6df8916cc5b7e3e96ee4c44c936717d684a94f48f82edb7c92f"}, - {file = "websockets-13.1-cp311-cp311-win_amd64.whl", hash = "sha256:d04f13a1d75cb2b8382bdc16ae6fa58c97337253826dfe136195b7f89f661557"}, - {file = "websockets-13.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9d75baf00138f80b48f1eac72ad1535aac0b6461265a0bcad391fc5aba875cfc"}, - {file = "websockets-13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9b6f347deb3dcfbfde1c20baa21c2ac0751afaa73e64e5b693bb2b848efeaa49"}, - {file = "websockets-13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de58647e3f9c42f13f90ac7e5f58900c80a39019848c5547bc691693098ae1bd"}, - {file = "websockets-13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1b54689e38d1279a51d11e3467dd2f3a50f5f2e879012ce8f2d6943f00e83f0"}, - {file = "websockets-13.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf1781ef73c073e6b0f90af841aaf98501f975d306bbf6221683dd594ccc52b6"}, - {file = "websockets-13.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d23b88b9388ed85c6faf0e74d8dec4f4d3baf3ecf20a65a47b836d56260d4b9"}, - {file = "websockets-13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3c78383585f47ccb0fcf186dcb8a43f5438bd7d8f47d69e0b56f71bf431a0a68"}, - {file = "websockets-13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d6d300f8ec35c24025ceb9b9019ae9040c1ab2f01cddc2bcc0b518af31c75c14"}, - {file = "websockets-13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a9dcaf8b0cc72a392760bb8755922c03e17a5a54e08cca58e8b74f6902b433cf"}, - {file = "websockets-13.1-cp312-cp312-win32.whl", hash = "sha256:2f85cf4f2a1ba8f602298a853cec8526c2ca42a9a4b947ec236eaedb8f2dc80c"}, - {file = "websockets-13.1-cp312-cp312-win_amd64.whl", hash = "sha256:38377f8b0cdeee97c552d20cf1865695fcd56aba155ad1b4ca8779a5b6ef4ac3"}, - {file = "websockets-13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a9ab1e71d3d2e54a0aa646ab6d4eebfaa5f416fe78dfe4da2839525dc5d765c6"}, - {file = "websockets-13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b9d7439d7fab4dce00570bb906875734df13d9faa4b48e261c440a5fec6d9708"}, - {file = "websockets-13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327b74e915cf13c5931334c61e1a41040e365d380f812513a255aa804b183418"}, - {file = "websockets-13.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:325b1ccdbf5e5725fdcb1b0e9ad4d2545056479d0eee392c291c1bf76206435a"}, - {file = "websockets-13.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:346bee67a65f189e0e33f520f253d5147ab76ae42493804319b5716e46dddf0f"}, - {file = "websockets-13.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91a0fa841646320ec0d3accdff5b757b06e2e5c86ba32af2e0815c96c7a603c5"}, - {file = "websockets-13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:18503d2c5f3943e93819238bf20df71982d193f73dcecd26c94514f417f6b135"}, - {file = "websockets-13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9cd1af7e18e5221d2878378fbc287a14cd527fdd5939ed56a18df8a31136bb2"}, - {file = "websockets-13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:70c5be9f416aa72aab7a2a76c90ae0a4fe2755c1816c153c1a2bcc3333ce4ce6"}, - {file = "websockets-13.1-cp313-cp313-win32.whl", hash = "sha256:624459daabeb310d3815b276c1adef475b3e6804abaf2d9d2c061c319f7f187d"}, - {file = "websockets-13.1-cp313-cp313-win_amd64.whl", hash = "sha256:c518e84bb59c2baae725accd355c8dc517b4a3ed8db88b4bc93c78dae2974bf2"}, - {file = "websockets-13.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c7934fd0e920e70468e676fe7f1b7261c1efa0d6c037c6722278ca0228ad9d0d"}, - {file = "websockets-13.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:149e622dc48c10ccc3d2760e5f36753db9cacf3ad7bc7bbbfd7d9c819e286f23"}, - {file = "websockets-13.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a569eb1b05d72f9bce2ebd28a1ce2054311b66677fcd46cf36204ad23acead8c"}, - {file = "websockets-13.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95df24ca1e1bd93bbca51d94dd049a984609687cb2fb08a7f2c56ac84e9816ea"}, - {file = "websockets-13.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8dbb1bf0c0a4ae8b40bdc9be7f644e2f3fb4e8a9aca7145bfa510d4a374eeb7"}, - {file = "websockets-13.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:035233b7531fb92a76beefcbf479504db8c72eb3bff41da55aecce3a0f729e54"}, - {file = "websockets-13.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:e4450fc83a3df53dec45922b576e91e94f5578d06436871dce3a6be38e40f5db"}, - {file = "websockets-13.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:463e1c6ec853202dd3657f156123d6b4dad0c546ea2e2e38be2b3f7c5b8e7295"}, - {file = "websockets-13.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6d6855bbe70119872c05107e38fbc7f96b1d8cb047d95c2c50869a46c65a8e96"}, - {file = "websockets-13.1-cp38-cp38-win32.whl", hash = "sha256:204e5107f43095012b00f1451374693267adbb832d29966a01ecc4ce1db26faf"}, - {file = "websockets-13.1-cp38-cp38-win_amd64.whl", hash = "sha256:485307243237328c022bc908b90e4457d0daa8b5cf4b3723fd3c4a8012fce4c6"}, - {file = "websockets-13.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9b37c184f8b976f0c0a231a5f3d6efe10807d41ccbe4488df8c74174805eea7d"}, - {file = "websockets-13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:163e7277e1a0bd9fb3c8842a71661ad19c6aa7bb3d6678dc7f89b17fbcc4aeb7"}, - {file = "websockets-13.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4b889dbd1342820cc210ba44307cf75ae5f2f96226c0038094455a96e64fb07a"}, - {file = "websockets-13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:586a356928692c1fed0eca68b4d1c2cbbd1ca2acf2ac7e7ebd3b9052582deefa"}, - {file = "websockets-13.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7bd6abf1e070a6b72bfeb71049d6ad286852e285f146682bf30d0296f5fbadfa"}, - {file = "websockets-13.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2aad13a200e5934f5a6767492fb07151e1de1d6079c003ab31e1823733ae79"}, - {file = "websockets-13.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:df01aea34b6e9e33572c35cd16bae5a47785e7d5c8cb2b54b2acdb9678315a17"}, - {file = "websockets-13.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e54affdeb21026329fb0744ad187cf812f7d3c2aa702a5edb562b325191fcab6"}, - {file = "websockets-13.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9ef8aa8bdbac47f4968a5d66462a2a0935d044bf35c0e5a8af152d58516dbeb5"}, - {file = "websockets-13.1-cp39-cp39-win32.whl", hash = "sha256:deeb929efe52bed518f6eb2ddc00cc496366a14c726005726ad62c2dd9017a3c"}, - {file = "websockets-13.1-cp39-cp39-win_amd64.whl", hash = "sha256:7c65ffa900e7cc958cd088b9a9157a8141c991f8c53d11087e6fb7277a03f81d"}, - {file = "websockets-13.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5dd6da9bec02735931fccec99d97c29f47cc61f644264eb995ad6c0c27667238"}, - {file = "websockets-13.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:2510c09d8e8df777177ee3d40cd35450dc169a81e747455cc4197e63f7e7bfe5"}, - {file = "websockets-13.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1c3cf67185543730888b20682fb186fc8d0fa6f07ccc3ef4390831ab4b388d9"}, - {file = "websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcc03c8b72267e97b49149e4863d57c2d77f13fae12066622dc78fe322490fe6"}, - {file = "websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:004280a140f220c812e65f36944a9ca92d766b6cc4560be652a0a3883a79ed8a"}, - {file = "websockets-13.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e2620453c075abeb0daa949a292e19f56de518988e079c36478bacf9546ced23"}, - {file = "websockets-13.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9156c45750b37337f7b0b00e6248991a047be4aa44554c9886fe6bdd605aab3b"}, - {file = "websockets-13.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:80c421e07973a89fbdd93e6f2003c17d20b69010458d3a8e37fb47874bd67d51"}, - {file = "websockets-13.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82d0ba76371769d6a4e56f7e83bb8e81846d17a6190971e38b5de108bde9b0d7"}, - {file = "websockets-13.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9875a0143f07d74dc5e1ded1c4581f0d9f7ab86c78994e2ed9e95050073c94d"}, - {file = "websockets-13.1-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a11e38ad8922c7961447f35c7b17bffa15de4d17c70abd07bfbe12d6faa3e027"}, - {file = "websockets-13.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4059f790b6ae8768471cddb65d3c4fe4792b0ab48e154c9f0a04cefaabcd5978"}, - {file = "websockets-13.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:25c35bf84bf7c7369d247f0b8cfa157f989862c49104c5cf85cb5436a641d93e"}, - {file = "websockets-13.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:83f91d8a9bb404b8c2c41a707ac7f7f75b9442a0a876df295de27251a856ad09"}, - {file = "websockets-13.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a43cfdcddd07f4ca2b1afb459824dd3c6d53a51410636a2c7fc97b9a8cf4842"}, - {file = "websockets-13.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48a2ef1381632a2f0cb4efeff34efa97901c9fbc118e01951ad7cfc10601a9bb"}, - {file = "websockets-13.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459bf774c754c35dbb487360b12c5727adab887f1622b8aed5755880a21c4a20"}, - {file = "websockets-13.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:95858ca14a9f6fa8413d29e0a585b31b278388aa775b8a81fa24830123874678"}, - {file = "websockets-13.1-py3-none-any.whl", hash = "sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f"}, - {file = "websockets-13.1.tar.gz", hash = "sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878"}, + {file = "websockets-10.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d58804e996d7d2307173d56c297cf7bc132c52df27a3efaac5e8d43e36c21c48"}, + {file = "websockets-10.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc0b82d728fe21a0d03e65f81980abbbcb13b5387f733a1a870672c5be26edab"}, + {file = "websockets-10.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ba089c499e1f4155d2a3c2a05d2878a3428cf321c848f2b5a45ce55f0d7d310c"}, + {file = "websockets-10.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33d69ca7612f0ddff3316b0c7b33ca180d464ecac2d115805c044bf0a3b0d032"}, + {file = "websockets-10.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62e627f6b6d4aed919a2052efc408da7a545c606268d5ab5bfab4432734b82b4"}, + {file = "websockets-10.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ea7b82bfcae927eeffc55d2ffa31665dc7fec7b8dc654506b8e5a518eb4d50"}, + {file = "websockets-10.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e0cb5cc6ece6ffa75baccfd5c02cffe776f3f5c8bf486811f9d3ea3453676ce8"}, + {file = "websockets-10.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ae5e95cfb53ab1da62185e23b3130e11d64431179debac6dc3c6acf08760e9b1"}, + {file = "websockets-10.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7c584f366f46ba667cfa66020344886cf47088e79c9b9d39c84ce9ea98aaa331"}, + {file = "websockets-10.4-cp310-cp310-win32.whl", hash = "sha256:b029fb2032ae4724d8ae8d4f6b363f2cc39e4c7b12454df8df7f0f563ed3e61a"}, + {file = "websockets-10.4-cp310-cp310-win_amd64.whl", hash = "sha256:8dc96f64ae43dde92530775e9cb169979f414dcf5cff670455d81a6823b42089"}, + {file = "websockets-10.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47a2964021f2110116cc1125b3e6d87ab5ad16dea161949e7244ec583b905bb4"}, + {file = "websockets-10.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e789376b52c295c4946403bd0efecf27ab98f05319df4583d3c48e43c7342c2f"}, + {file = "websockets-10.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d3f0b61c45c3fa9a349cf484962c559a8a1d80dae6977276df8fd1fa5e3cb8c"}, + {file = "websockets-10.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f55b5905705725af31ccef50e55391621532cd64fbf0bc6f4bac935f0fccec46"}, + {file = "websockets-10.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00c870522cdb69cd625b93f002961ffb0c095394f06ba8c48f17eef7c1541f96"}, + {file = "websockets-10.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f38706e0b15d3c20ef6259fd4bc1700cd133b06c3c1bb108ffe3f8947be15fa"}, + {file = "websockets-10.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f2c38d588887a609191d30e902df2a32711f708abfd85d318ca9b367258cfd0c"}, + {file = "websockets-10.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:fe10ddc59b304cb19a1bdf5bd0a7719cbbc9fbdd57ac80ed436b709fcf889106"}, + {file = "websockets-10.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:90fcf8929836d4a0e964d799a58823547df5a5e9afa83081761630553be731f9"}, + {file = "websockets-10.4-cp311-cp311-win32.whl", hash = "sha256:b9968694c5f467bf67ef97ae7ad4d56d14be2751000c1207d31bf3bb8860bae8"}, + {file = "websockets-10.4-cp311-cp311-win_amd64.whl", hash = "sha256:a7a240d7a74bf8d5cb3bfe6be7f21697a28ec4b1a437607bae08ac7acf5b4882"}, + {file = "websockets-10.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:74de2b894b47f1d21cbd0b37a5e2b2392ad95d17ae983e64727e18eb281fe7cb"}, + {file = "websockets-10.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3a686ecb4aa0d64ae60c9c9f1a7d5d46cab9bfb5d91a2d303d00e2cd4c4c5cc"}, + {file = "websockets-10.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d15c968ea7a65211e084f523151dbf8ae44634de03c801b8bd070b74e85033"}, + {file = "websockets-10.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00213676a2e46b6ebf6045bc11d0f529d9120baa6f58d122b4021ad92adabd41"}, + {file = "websockets-10.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:e23173580d740bf8822fd0379e4bf30aa1d5a92a4f252d34e893070c081050df"}, + {file = "websockets-10.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:dd500e0a5e11969cdd3320935ca2ff1e936f2358f9c2e61f100a1660933320ea"}, + {file = "websockets-10.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4239b6027e3d66a89446908ff3027d2737afc1a375f8fd3eea630a4842ec9a0c"}, + {file = "websockets-10.4-cp37-cp37m-win32.whl", hash = "sha256:8a5cc00546e0a701da4639aa0bbcb0ae2bb678c87f46da01ac2d789e1f2d2038"}, + {file = "websockets-10.4-cp37-cp37m-win_amd64.whl", hash = "sha256:a9f9a735deaf9a0cadc2d8c50d1a5bcdbae8b6e539c6e08237bc4082d7c13f28"}, + {file = "websockets-10.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5c1289596042fad2cdceb05e1ebf7aadf9995c928e0da2b7a4e99494953b1b94"}, + {file = "websockets-10.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0cff816f51fb33c26d6e2b16b5c7d48eaa31dae5488ace6aae468b361f422b63"}, + {file = "websockets-10.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dd9becd5fe29773d140d68d607d66a38f60e31b86df75332703757ee645b6faf"}, + {file = "websockets-10.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45ec8e75b7dbc9539cbfafa570742fe4f676eb8b0d3694b67dabe2f2ceed8aa6"}, + {file = "websockets-10.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f72e5cd0f18f262f5da20efa9e241699e0cf3a766317a17392550c9ad7b37d8"}, + {file = "websockets-10.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185929b4808b36a79c65b7865783b87b6841e852ef5407a2fb0c03381092fa3b"}, + {file = "websockets-10.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7d27a7e34c313b3a7f91adcd05134315002aaf8540d7b4f90336beafaea6217c"}, + {file = "websockets-10.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:884be66c76a444c59f801ac13f40c76f176f1bfa815ef5b8ed44321e74f1600b"}, + {file = "websockets-10.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:931c039af54fc195fe6ad536fde4b0de04da9d5916e78e55405436348cfb0e56"}, + {file = "websockets-10.4-cp38-cp38-win32.whl", hash = "sha256:db3c336f9eda2532ec0fd8ea49fef7a8df8f6c804cdf4f39e5c5c0d4a4ad9a7a"}, + {file = "websockets-10.4-cp38-cp38-win_amd64.whl", hash = "sha256:48c08473563323f9c9debac781ecf66f94ad5a3680a38fe84dee5388cf5acaf6"}, + {file = "websockets-10.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:40e826de3085721dabc7cf9bfd41682dadc02286d8cf149b3ad05bff89311e4f"}, + {file = "websockets-10.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:56029457f219ade1f2fc12a6504ea61e14ee227a815531f9738e41203a429112"}, + {file = "websockets-10.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f5fc088b7a32f244c519a048c170f14cf2251b849ef0e20cbbb0fdf0fdaf556f"}, + {file = "websockets-10.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fc8709c00704194213d45e455adc106ff9e87658297f72d544220e32029cd3d"}, + {file = "websockets-10.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0154f7691e4fe6c2b2bc275b5701e8b158dae92a1ab229e2b940efe11905dff4"}, + {file = "websockets-10.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c6d2264f485f0b53adf22697ac11e261ce84805c232ed5dbe6b1bcb84b00ff0"}, + {file = "websockets-10.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9bc42e8402dc5e9905fb8b9649f57efcb2056693b7e88faa8fb029256ba9c68c"}, + {file = "websockets-10.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:edc344de4dac1d89300a053ac973299e82d3db56330f3494905643bb68801269"}, + {file = "websockets-10.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:84bc2a7d075f32f6ed98652db3a680a17a4edb21ca7f80fe42e38753a58ee02b"}, + {file = "websockets-10.4-cp39-cp39-win32.whl", hash = "sha256:c94ae4faf2d09f7c81847c63843f84fe47bf6253c9d60b20f25edfd30fb12588"}, + {file = "websockets-10.4-cp39-cp39-win_amd64.whl", hash = "sha256:bbccd847aa0c3a69b5f691a84d2341a4f8a629c6922558f2a70611305f902d74"}, + {file = "websockets-10.4-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:82ff5e1cae4e855147fd57a2863376ed7454134c2bf49ec604dfe71e446e2193"}, + {file = "websockets-10.4-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d210abe51b5da0ffdbf7b43eed0cfdff8a55a1ab17abbec4301c9ff077dd0342"}, + {file = "websockets-10.4-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:942de28af58f352a6f588bc72490ae0f4ccd6dfc2bd3de5945b882a078e4e179"}, + {file = "websockets-10.4-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9b27d6c1c6cd53dc93614967e9ce00ae7f864a2d9f99fe5ed86706e1ecbf485"}, + {file = "websockets-10.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:3d3cac3e32b2c8414f4f87c1b2ab686fa6284a980ba283617404377cd448f631"}, + {file = "websockets-10.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:da39dd03d130162deb63da51f6e66ed73032ae62e74aaccc4236e30edccddbb0"}, + {file = "websockets-10.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389f8dbb5c489e305fb113ca1b6bdcdaa130923f77485db5b189de343a179393"}, + {file = "websockets-10.4-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09a1814bb15eff7069e51fed0826df0bc0702652b5cb8f87697d469d79c23576"}, + {file = "websockets-10.4-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff64a1d38d156d429404aaa84b27305e957fd10c30e5880d1765c9480bea490f"}, + {file = "websockets-10.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b343f521b047493dc4022dd338fc6db9d9282658862756b4f6fd0e996c1380e1"}, + {file = "websockets-10.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:932af322458da7e4e35df32f050389e13d3d96b09d274b22a7aa1808f292fee4"}, + {file = "websockets-10.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a4162139374a49eb18ef5b2f4da1dd95c994588f5033d64e0bbfda4b6b6fcf"}, + {file = "websockets-10.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c57e4c1349fbe0e446c9fa7b19ed2f8a4417233b6984277cce392819123142d3"}, + {file = "websockets-10.4-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b627c266f295de9dea86bd1112ed3d5fafb69a348af30a2422e16590a8ecba13"}, + {file = "websockets-10.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:05a7233089f8bd355e8cbe127c2e8ca0b4ea55467861906b80d2ebc7db4d6b72"}, + {file = "websockets-10.4.tar.gz", hash = "sha256:eef610b23933c54d5d921c92578ae5f89813438fded840c2e9809d378dc765d3"}, ] [[package]] @@ -3443,112 +4849,172 @@ files = [ [package.extras] watchdog = ["watchdog"] +[[package]] +name = "ws4py" +version = "0.5.1" +description = "WebSocket client and server library for Python 2 and 3 as well as PyPy" +optional = false +python-versions = "*" +files = [ + {file = "ws4py-0.5.1.tar.gz", hash = "sha256:29d073d7f2e006373e6a848b1d00951a1107eb81f3742952be905429dc5a5483"}, +] + [[package]] name = "yarl" -version = "1.12.1" +version = "1.16.0" description = "Yet another URL library" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "yarl-1.12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:64c5b0f2b937fe40d0967516eee5504b23cb247b8b7ffeba7213a467d9646fdc"}, - {file = "yarl-1.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2e430ac432f969ef21770645743611c1618362309e3ad7cab45acd1ad1a540ff"}, - {file = "yarl-1.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3e26e64f42bce5ddf9002092b2c37b13071c2e6413d5c05f9fa9de58ed2f7749"}, - {file = "yarl-1.12.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0103c52f8dfe5d573c856322149ddcd6d28f51b4d4a3ee5c4b3c1b0a05c3d034"}, - {file = "yarl-1.12.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b63465b53baeaf2122a337d4ab57d6bbdd09fcadceb17a974cfa8a0300ad9c67"}, - {file = "yarl-1.12.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17d4dc4ff47893a06737b8788ed2ba2f5ac4e8bb40281c8603920f7d011d5bdd"}, - {file = "yarl-1.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b54949267bd5704324397efe9fbb6aa306466dee067550964e994d309db5f1"}, - {file = "yarl-1.12.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10b690cd78cbaca2f96a7462f303fdd2b596d3978b49892e4b05a7567c591572"}, - {file = "yarl-1.12.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c85ab016e96a975afbdb9d49ca90f3bca9920ef27c64300843fe91c3d59d8d20"}, - {file = "yarl-1.12.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c1caa5763d1770216596e0a71b5567f27aac28c95992110212c108ec74589a48"}, - {file = "yarl-1.12.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:595bbcdbfc4a9c6989d7489dca8510cba053ff46b16c84ffd95ac8e90711d419"}, - {file = "yarl-1.12.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e64f0421892a207d3780903085c1b04efeb53b16803b23d947de5a7261b71355"}, - {file = "yarl-1.12.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:319c206e83e46ec2421b25b300c8482b6fe8a018baca246be308c736d9dab267"}, - {file = "yarl-1.12.1-cp310-cp310-win32.whl", hash = "sha256:da045bd1147d12bd43fb032296640a7cc17a7f2eaba67495988362e99db24fd2"}, - {file = "yarl-1.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:aebbd47df77190ada603157f0b3670d578c110c31746ecc5875c394fdcc59a99"}, - {file = "yarl-1.12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:28389a68981676bf74e2e199fe42f35d1aa27a9c98e3a03e6f58d2d3d054afe1"}, - {file = "yarl-1.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f736f54565f8dd7e3ab664fef2bc461d7593a389a7f28d4904af8d55a91bd55f"}, - {file = "yarl-1.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dee0496d5f1a8f57f0f28a16f81a2033fc057a2cf9cd710742d11828f8c80e2"}, - {file = "yarl-1.12.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8981a94a27ac520a398302afb74ae2c0be1c3d2d215c75c582186a006c9e7b0"}, - {file = "yarl-1.12.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff54340fc1129e8e181827e2234af3ff659b4f17d9bbe77f43bc19e6577fadec"}, - {file = "yarl-1.12.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54c8cee662b5f8c30ad7eedfc26123f845f007798e4ff1001d9528fe959fd23c"}, - {file = "yarl-1.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e97a29b37830ba1262d8dfd48ddb5b28ad4d3ebecc5d93a9c7591d98641ec737"}, - {file = "yarl-1.12.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c89894cc6f6ddd993813e79244b36b215c14f65f9e4f1660b1f2ba9e5594b95"}, - {file = "yarl-1.12.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:712ba8722c0699daf186de089ddc4677651eb9875ed7447b2ad50697522cbdd9"}, - {file = "yarl-1.12.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6e9a9f50892153bad5046c2a6df153224aa6f0573a5a8ab44fc54a1e886f6e21"}, - {file = "yarl-1.12.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1d4017e78fb22bc797c089b746230ad78ecd3cdb215bc0bd61cb72b5867da57e"}, - {file = "yarl-1.12.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f494c01b28645c431239863cb17af8b8d15b93b0d697a0320d5dd34cd9d7c2fa"}, - {file = "yarl-1.12.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:de4544b1fb29cf14870c4e2b8a897c0242449f5dcebd3e0366aa0aa3cf58a23a"}, - {file = "yarl-1.12.1-cp311-cp311-win32.whl", hash = "sha256:7564525a4673fde53dee7d4c307a961c0951918f0b8c7f09b2c9e02067cf6504"}, - {file = "yarl-1.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:f23bb1a7a6e8e8b612a164fdd08e683bcc16c76f928d6dbb7bdbee2374fbfee6"}, - {file = "yarl-1.12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a3e2aff8b822ab0e0bdbed9f50494b3a35629c4b9488ae391659973a37a9f53f"}, - {file = "yarl-1.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:22dda2799c8d39041d731e02bf7690f0ef34f1691d9ac9dfcb98dd1e94c8b058"}, - {file = "yarl-1.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18c2a7757561f05439c243f517dbbb174cadfae3a72dee4ae7c693f5b336570f"}, - {file = "yarl-1.12.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:835010cc17d0020e7931d39e487d72c8e01c98e669b6896a8b8c9aa8ca69a949"}, - {file = "yarl-1.12.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2254fe137c4a360b0a13173a56444f756252c9283ba4d267ca8e9081cd140ea"}, - {file = "yarl-1.12.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6a071d2c3d39b4104f94fc08ab349e9b19b951ad4b8e3b6d7ea92d6ef7ccaf8"}, - {file = "yarl-1.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73a183042ae0918c82ce2df38c3db2409b0eeae88e3afdfc80fb67471a95b33b"}, - {file = "yarl-1.12.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:326b8a079a9afcac0575971e56dabdf7abb2ea89a893e6949b77adfeb058b50e"}, - {file = "yarl-1.12.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:126309c0f52a2219b3d1048aca00766429a1346596b186d51d9fa5d2070b7b13"}, - {file = "yarl-1.12.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ba1c779b45a399cc25f511c681016626f69e51e45b9d350d7581998722825af9"}, - {file = "yarl-1.12.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:af1107299cef049ad00a93df4809517be432283a0847bcae48343ebe5ea340dc"}, - {file = "yarl-1.12.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:20d817c0893191b2ab0ba30b45b77761e8dfec30a029b7c7063055ca71157f84"}, - {file = "yarl-1.12.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d4f818f6371970d6a5d1e42878389bbfb69dcde631e4bbac5ec1cb11158565ca"}, - {file = "yarl-1.12.1-cp312-cp312-win32.whl", hash = "sha256:0ac33d22b2604b020569a82d5f8a03ba637ba42cc1adf31f616af70baf81710b"}, - {file = "yarl-1.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:fd24996e12e1ba7c397c44be75ca299da14cde34d74bc5508cce233676cc68d0"}, - {file = "yarl-1.12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dea360778e0668a7ad25d7727d03364de8a45bfd5d808f81253516b9f2217765"}, - {file = "yarl-1.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1f50a37aeeb5179d293465e522fd686080928c4d89e0ff215e1f963405ec4def"}, - {file = "yarl-1.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0274b1b7a9c9c32b7bf250583e673ff99fb9fccb389215841e2652d9982de740"}, - {file = "yarl-1.12.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4f3ab9eb8ab2d585ece959c48d234f7b39ac0ca1954a34d8b8e58a52064bdb3"}, - {file = "yarl-1.12.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d31dd0245d88cf7239e96e8f2a99f815b06e458a5854150f8e6f0e61618d41b"}, - {file = "yarl-1.12.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a96198d5d26f40557d986c1253bfe0e02d18c9d9b93cf389daf1a3c9f7c755fa"}, - {file = "yarl-1.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddae504cfb556fe220efae65e35be63cd11e3c314b202723fc2119ce19f0ca2e"}, - {file = "yarl-1.12.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bce00f3b1f7f644faae89677ca68645ed5365f1c7f874fdd5ebf730a69640d38"}, - {file = "yarl-1.12.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eee5ff934b0c9f4537ff9596169d56cab1890918004791a7a06b879b3ba2a7ef"}, - {file = "yarl-1.12.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4ea99e64b2ad2635e0f0597b63f5ea6c374791ff2fa81cdd4bad8ed9f047f56f"}, - {file = "yarl-1.12.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c667b383529520b8dd6bd496fc318678320cb2a6062fdfe6d3618da6b8790f6"}, - {file = "yarl-1.12.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d920401941cb898ef089422e889759dd403309eb370d0e54f1bdf6ca07fef603"}, - {file = "yarl-1.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:501a1576716032cc6d48c7c47bcdc42d682273415a8f2908e7e72cb4625801f3"}, - {file = "yarl-1.12.1-cp313-cp313-win32.whl", hash = "sha256:24416bb5e221e29ddf8aac5b97e94e635ca2c5be44a1617ad6fe32556df44294"}, - {file = "yarl-1.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:71af3766bb46738d12cc288d9b8de7ef6f79c31fd62757e2b8a505fe3680b27f"}, - {file = "yarl-1.12.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c924deab8105f86980983eced740433fb7554a7f66db73991affa4eda99d5402"}, - {file = "yarl-1.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5fb475a4cdde582c9528bb412b98f899680492daaba318231e96f1a0a1bb0d53"}, - {file = "yarl-1.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:36ee0115b9edca904153a66bb74a9ff1ce38caff015de94eadfb9ba8e6ecd317"}, - {file = "yarl-1.12.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2631c9d7386bd2d4ce24ecc6ebf9ae90b3efd713d588d90504eaa77fec4dba01"}, - {file = "yarl-1.12.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2376d8cf506dffd0e5f2391025ae8675b09711016656590cb03b55894161fcfa"}, - {file = "yarl-1.12.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24197ba3114cc85ddd4091e19b2ddc62650f2e4a899e51b074dfd52d56cf8c72"}, - {file = "yarl-1.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfdf419bf5d3644f94cd7052954fc233522f5a1b371fc0b00219ebd9c14d5798"}, - {file = "yarl-1.12.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8112f640a4f7e7bf59f7cabf0d47a29b8977528c521d73a64d5cc9e99e48a174"}, - {file = "yarl-1.12.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:607d12f0901f6419a8adceb139847c42c83864b85371f58270e42753f9780fa6"}, - {file = "yarl-1.12.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:664380c7ed524a280b6a2d5d9126389c3e96cd6e88986cdb42ca72baa27421d6"}, - {file = "yarl-1.12.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:0d0a5e87bc48d76dfcfc16295201e9812d5f33d55b4a0b7cad1025b92bf8b91b"}, - {file = "yarl-1.12.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:eff6bac402719c14e17efe845d6b98593c56c843aca6def72080fbede755fd1f"}, - {file = "yarl-1.12.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:22839d1d1eab9e4b427828a88a22beb86f67c14d8ff81175505f1cc8493f3500"}, - {file = "yarl-1.12.1-cp38-cp38-win32.whl", hash = "sha256:717f185086bb9d817d4537dd18d5df5d657598cd00e6fc22e4d54d84de266c1d"}, - {file = "yarl-1.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:71978ba778948760cff528235c951ea0ef7a4f9c84ac5a49975f8540f76c3f73"}, - {file = "yarl-1.12.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:30ffc046ebddccb3c4cac72c1a3e1bc343492336f3ca86d24672e90ccc5e788a"}, - {file = "yarl-1.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f10954b233d4df5cc3137ffa5ced97f8894152df817e5d149bf05a0ef2ab8134"}, - {file = "yarl-1.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2e912b282466444023610e4498e3795c10e7cfd641744524876239fcf01d538d"}, - {file = "yarl-1.12.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6af871f70cfd5b528bd322c65793b5fd5659858cdfaa35fbe563fb99b667ed1f"}, - {file = "yarl-1.12.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3e4e1f7b08d1ec6b685ccd3e2d762219c550164fbf524498532e39f9413436e"}, - {file = "yarl-1.12.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a7ee79183f0b17dcede8b6723e7da2ded529cf159a878214be9a5d3098f5b1e"}, - {file = "yarl-1.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96c8ff1e1dd680e38af0887927cab407a4e51d84a5f02ae3d6eb87233036c763"}, - {file = "yarl-1.12.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e9905fc2dc1319e4c39837b906a024cf71b1261cc66b0cd89678f779c0c61f5"}, - {file = "yarl-1.12.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:01549468858b87d36f967c97d02e6e54106f444aeb947ed76f8f71f85ed07cec"}, - {file = "yarl-1.12.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:96b34830bd6825ca0220bf005ea99ac83eb9ce51301ddb882dcf613ae6cd95fb"}, - {file = "yarl-1.12.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2aee7594d2c2221c717a8e394bbed4740029df4c0211ceb0f04815686e99c795"}, - {file = "yarl-1.12.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:15871130439ad10abb25a4631120d60391aa762b85fcab971411e556247210a0"}, - {file = "yarl-1.12.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:838dde2cb570cfbb4cab8a876a0974e8b90973ea40b3ac27a79b8a74c8a2db15"}, - {file = "yarl-1.12.1-cp39-cp39-win32.whl", hash = "sha256:eacbcf30efaca7dc5cb264228ffecdb95fdb1e715b1ec937c0ce6b734161e0c8"}, - {file = "yarl-1.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:76a59d1b63de859398bc7764c860a769499511463c1232155061fe0147f13e01"}, - {file = "yarl-1.12.1-py3-none-any.whl", hash = "sha256:dc3192a81ecd5ff954cecd690327badd5a84d00b877e1573f7c9097ce13e5bfb"}, - {file = "yarl-1.12.1.tar.gz", hash = "sha256:5b860055199aec8d6fe4dcee3c5196ce506ca198a50aab0059ffd26e8e815828"}, + {file = "yarl-1.16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32468f41242d72b87ab793a86d92f885355bcf35b3355aa650bfa846a5c60058"}, + {file = "yarl-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:234f3a3032b505b90e65b5bc6652c2329ea7ea8855d8de61e1642b74b4ee65d2"}, + {file = "yarl-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a0296040e5cddf074c7f5af4a60f3fc42c0237440df7bcf5183be5f6c802ed5"}, + {file = "yarl-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de6c14dd7c7c0badba48157474ea1f03ebee991530ba742d381b28d4f314d6f3"}, + {file = "yarl-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b140e532fe0266003c936d017c1ac301e72ee4a3fd51784574c05f53718a55d8"}, + {file = "yarl-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:019f5d58093402aa8f6661e60fd82a28746ad6d156f6c5336a70a39bd7b162b9"}, + {file = "yarl-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c42998fd1cbeb53cd985bff0e4bc25fbe55fd6eb3a545a724c1012d69d5ec84"}, + {file = "yarl-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c7c30fb38c300fe8140df30a046a01769105e4cf4282567a29b5cdb635b66c4"}, + {file = "yarl-1.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e49e0fd86c295e743fd5be69b8b0712f70a686bc79a16e5268386c2defacaade"}, + {file = "yarl-1.16.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:b9ca7b9147eb1365c8bab03c003baa1300599575effad765e0b07dd3501ea9af"}, + {file = "yarl-1.16.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:27e11db3f1e6a51081a981509f75617b09810529de508a181319193d320bc5c7"}, + {file = "yarl-1.16.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8994c42f4ca25df5380ddf59f315c518c81df6a68fed5bb0c159c6cb6b92f120"}, + {file = "yarl-1.16.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:542fa8e09a581bcdcbb30607c7224beff3fdfb598c798ccd28a8184ffc18b7eb"}, + {file = "yarl-1.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2bd6a51010c7284d191b79d3b56e51a87d8e1c03b0902362945f15c3d50ed46b"}, + {file = "yarl-1.16.0-cp310-cp310-win32.whl", hash = "sha256:178ccb856e265174a79f59721031060f885aca428983e75c06f78aa24b91d929"}, + {file = "yarl-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe8bba2545427418efc1929c5c42852bdb4143eb8d0a46b09de88d1fe99258e7"}, + {file = "yarl-1.16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d8643975a0080f361639787415a038bfc32d29208a4bf6b783ab3075a20b1ef3"}, + {file = "yarl-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:676d96bafc8c2d0039cea0cd3fd44cee7aa88b8185551a2bb93354668e8315c2"}, + {file = "yarl-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d9525f03269e64310416dbe6c68d3b23e5d34aaa8f47193a1c45ac568cecbc49"}, + {file = "yarl-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b37d5ec034e668b22cf0ce1074d6c21fd2a08b90d11b1b73139b750a8b0dd97"}, + {file = "yarl-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f32c4cb7386b41936894685f6e093c8dfaf0960124d91fe0ec29fe439e201d0"}, + {file = "yarl-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b8e265a0545637492a7e12fd7038370d66c9375a61d88c5567d0e044ded9202"}, + {file = "yarl-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:789a3423f28a5fff46fbd04e339863c169ece97c827b44de16e1a7a42bc915d2"}, + {file = "yarl-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1d1f45e3e8d37c804dca99ab3cf4ab3ed2e7a62cd82542924b14c0a4f46d243"}, + {file = "yarl-1.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:621280719c4c5dad4c1391160a9b88925bb8b0ff6a7d5af3224643024871675f"}, + {file = "yarl-1.16.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ed097b26f18a1f5ff05f661dc36528c5f6735ba4ce8c9645e83b064665131349"}, + {file = "yarl-1.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2f1fe2b2e3ee418862f5ebc0c0083c97f6f6625781382f828f6d4e9b614eba9b"}, + {file = "yarl-1.16.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:87dd10bc0618991c66cee0cc65fa74a45f4ecb13bceec3c62d78ad2e42b27a16"}, + {file = "yarl-1.16.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4199db024b58a8abb2cfcedac7b1292c3ad421684571aeb622a02f242280e8d6"}, + {file = "yarl-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:99a9dcd4b71dd5f5f949737ab3f356cfc058c709b4f49833aeffedc2652dac56"}, + {file = "yarl-1.16.0-cp311-cp311-win32.whl", hash = "sha256:a9394c65ae0ed95679717d391c862dece9afacd8fa311683fc8b4362ce8a410c"}, + {file = "yarl-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:5b9101f528ae0f8f65ac9d64dda2bb0627de8a50344b2f582779f32fda747c1d"}, + {file = "yarl-1.16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4ffb7c129707dd76ced0a4a4128ff452cecf0b0e929f2668ea05a371d9e5c104"}, + {file = "yarl-1.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1a5e9d8ce1185723419c487758d81ac2bde693711947032cce600ca7c9cda7d6"}, + {file = "yarl-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d743e3118b2640cef7768ea955378c3536482d95550222f908f392167fe62059"}, + {file = "yarl-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26768342f256e6e3c37533bf9433f5f15f3e59e3c14b2409098291b3efaceacb"}, + {file = "yarl-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1b0796168b953bca6600c5f97f5ed407479889a36ad7d17183366260f29a6b9"}, + {file = "yarl-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:858728086914f3a407aa7979cab743bbda1fe2bdf39ffcd991469a370dd7414d"}, + {file = "yarl-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5570e6d47bcb03215baf4c9ad7bf7c013e56285d9d35013541f9ac2b372593e7"}, + {file = "yarl-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66ea8311422a7ba1fc79b4c42c2baa10566469fe5a78500d4e7754d6e6db8724"}, + {file = "yarl-1.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:649bddcedee692ee8a9b7b6e38582cb4062dc4253de9711568e5620d8707c2a3"}, + {file = "yarl-1.16.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3a91654adb7643cb21b46f04244c5a315a440dcad63213033826549fa2435f71"}, + {file = "yarl-1.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b439cae82034ade094526a8f692b9a2b5ee936452de5e4c5f0f6c48df23f8604"}, + {file = "yarl-1.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:571f781ae8ac463ce30bacebfaef2c6581543776d5970b2372fbe31d7bf31a07"}, + {file = "yarl-1.16.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:aa7943f04f36d6cafc0cf53ea89824ac2c37acbdb4b316a654176ab8ffd0f968"}, + {file = "yarl-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1a5cf32539373ff39d97723e39a9283a7277cbf1224f7aef0c56c9598b6486c3"}, + {file = "yarl-1.16.0-cp312-cp312-win32.whl", hash = "sha256:a5b6c09b9b4253d6a208b0f4a2f9206e511ec68dce9198e0fbec4f160137aa67"}, + {file = "yarl-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1208ca14eed2fda324042adf8d6c0adf4a31522fa95e0929027cd487875f0240"}, + {file = "yarl-1.16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5ace0177520bd4caa99295a9b6fb831d0e9a57d8e0501a22ffaa61b4c024283"}, + {file = "yarl-1.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7118bdb5e3ed81acaa2095cba7ec02a0fe74b52a16ab9f9ac8e28e53ee299732"}, + {file = "yarl-1.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38fec8a2a94c58bd47c9a50a45d321ab2285ad133adefbbadf3012c054b7e656"}, + {file = "yarl-1.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8791d66d81ee45866a7bb15a517b01a2bcf583a18ebf5d72a84e6064c417e64b"}, + {file = "yarl-1.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cf936ba67bc6c734f3aa1c01391da74ab7fc046a9f8bbfa230b8393b90cf472"}, + {file = "yarl-1.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1aab176dd55b59f77a63b27cffaca67d29987d91a5b615cbead41331e6b7428"}, + {file = "yarl-1.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:995d0759004c08abd5d1b81300a91d18c8577c6389300bed1c7c11675105a44d"}, + {file = "yarl-1.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1bc22e00edeb068f71967ab99081e9406cd56dbed864fc3a8259442999d71552"}, + {file = "yarl-1.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:35b4f7842154176523e0a63c9b871168c69b98065d05a4f637fce342a6a2693a"}, + {file = "yarl-1.16.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:7ace71c4b7a0c41f317ae24be62bb61e9d80838d38acb20e70697c625e71f120"}, + {file = "yarl-1.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8f639e3f5795a6568aa4f7d2ac6057c757dcd187593679f035adbf12b892bb00"}, + {file = "yarl-1.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e8be3aff14f0120ad049121322b107f8a759be76a6a62138322d4c8a337a9e2c"}, + {file = "yarl-1.16.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:122d8e7986043d0549e9eb23c7fd23be078be4b70c9eb42a20052b3d3149c6f2"}, + {file = "yarl-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0fd9c227990f609c165f56b46107d0bc34553fe0387818c42c02f77974402c36"}, + {file = "yarl-1.16.0-cp313-cp313-win32.whl", hash = "sha256:595ca5e943baed31d56b33b34736461a371c6ea0038d3baec399949dd628560b"}, + {file = "yarl-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:921b81b8d78f0e60242fb3db615ea3f368827a76af095d5a69f1c3366db3f596"}, + {file = "yarl-1.16.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ab2b2ac232110a1fdb0d3ffcd087783edd3d4a6ced432a1bf75caf7b7be70916"}, + {file = "yarl-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7f8713717a09acbfee7c47bfc5777e685539fefdd34fa72faf504c8be2f3df4e"}, + {file = "yarl-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cdcffe1dbcb4477d2b4202f63cd972d5baa155ff5a3d9e35801c46a415b7f71a"}, + {file = "yarl-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a91217208306d82357c67daeef5162a41a28c8352dab7e16daa82e3718852a7"}, + {file = "yarl-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ab3ed42c78275477ea8e917491365e9a9b69bb615cb46169020bd0aa5e2d6d3"}, + {file = "yarl-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:707ae579ccb3262dfaef093e202b4c3fb23c3810e8df544b1111bd2401fd7b09"}, + {file = "yarl-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad7a852d1cd0b8d8b37fc9d7f8581152add917a98cfe2ea6e241878795f917ae"}, + {file = "yarl-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3f1cc3d3d4dc574bebc9b387f6875e228ace5748a7c24f49d8f01ac1bc6c31b"}, + {file = "yarl-1.16.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5ff96da263740779b0893d02b718293cc03400c3a208fc8d8cd79d9b0993e532"}, + {file = "yarl-1.16.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:3d375a19ba2bfe320b6d873f3fb165313b002cef8b7cc0a368ad8b8a57453837"}, + {file = "yarl-1.16.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:62c7da0ad93a07da048b500514ca47b759459ec41924143e2ddb5d7e20fd3db5"}, + {file = "yarl-1.16.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:147b0fcd0ee33b4b5f6edfea80452d80e419e51b9a3f7a96ce98eaee145c1581"}, + {file = "yarl-1.16.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:504e1fe1cc4f170195320eb033d2b0ccf5c6114ce5bf2f617535c01699479bca"}, + {file = "yarl-1.16.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:bdcf667a5dec12a48f669e485d70c54189f0639c2157b538a4cffd24a853624f"}, + {file = "yarl-1.16.0-cp39-cp39-win32.whl", hash = "sha256:e9951afe6557c75a71045148890052cb942689ee4c9ec29f5436240e1fcc73b7"}, + {file = "yarl-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:7d7aaa8ff95d0840e289423e7dc35696c2b058d635f945bf05b5cd633146b027"}, + {file = "yarl-1.16.0-py3-none-any.whl", hash = "sha256:e6980a558d8461230c457218bd6c92dfc1d10205548215c2c21d79dc8d0a96f3"}, + {file = "yarl-1.16.0.tar.gz", hash = "sha256:b6f687ced5510a9a2474bbae96a4352e5ace5fa34dc44a217b0537fec1db00b4"}, ] [package.dependencies] idna = ">=2.0" multidict = ">=4.0" +propcache = ">=0.2.0" + +[[package]] +name = "zstandard" +version = "0.18.0" +description = "Zstandard bindings for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "zstandard-0.18.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef7e8a200e4c8ac9102ed3c90ed2aa379f6b880f63032200909c1be21951f556"}, + {file = "zstandard-0.18.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2dc466207016564805e56d28375f4f533b525ff50d6776946980dff5465566ac"}, + {file = "zstandard-0.18.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a2ee1d4f98447f3e5183ecfce5626f983504a4a0c005fbe92e60fa8e5d547ec"}, + {file = "zstandard-0.18.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d956e2f03c7200d7e61345e0880c292783ec26618d0d921dcad470cb195bbce2"}, + {file = "zstandard-0.18.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ce6f59cba9854fd14da5bfe34217a1501143057313966637b7291d1b0267bd1e"}, + {file = "zstandard-0.18.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fa67cba473623848b6e88acf8d799b1906178fd883fb3a1da24561c779593b"}, + {file = "zstandard-0.18.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cdb44d7284c8c5dd1b66dfb86dda7f4560fa94bfbbc1d2da749ba44831335e32"}, + {file = "zstandard-0.18.0-cp310-cp310-win32.whl", hash = "sha256:63694a376cde0aa8b1971d06ca28e8f8b5f492779cb6ee1cc46bbc3f019a42a5"}, + {file = "zstandard-0.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:702a8324cd90c74d9c8780d02bf55e79da3193c870c9665ad3a11647e3ad1435"}, + {file = "zstandard-0.18.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:46f679bc5dfd938db4fb058218d9dc4db1336ffaf1ea774ff152ecadabd40805"}, + {file = "zstandard-0.18.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc2a4de9f363b3247d472362a65041fe4c0f59e01a2846b15d13046be866a885"}, + {file = "zstandard-0.18.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd3220d7627fd4d26397211cb3b560ec7cc4a94b75cfce89e847e8ce7fabe32d"}, + {file = "zstandard-0.18.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:39e98cf4773234bd9cebf9f9db730e451dfcfe435e220f8921242afda8321887"}, + {file = "zstandard-0.18.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5228e596eb1554598c872a337bbe4e5afe41cd1f8b1b15f2e35b50d061e35244"}, + {file = "zstandard-0.18.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d4a8fd45746a6c31e729f35196e80b8f1e9987c59f5ccb8859d7c6a6fbeb9c63"}, + {file = "zstandard-0.18.0-cp36-cp36m-win32.whl", hash = "sha256:4cbb85f29a990c2fdbf7bc63246567061a362ddca886d7fae6f780267c0a9e67"}, + {file = "zstandard-0.18.0-cp36-cp36m-win_amd64.whl", hash = "sha256:bfa6c8549fa18e6497a738b7033c49f94a8e2e30c5fbe2d14d0b5aa8bbc1695d"}, + {file = "zstandard-0.18.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e02043297c1832f2666cd2204f381bef43b10d56929e13c42c10c732c6e3b4ed"}, + {file = "zstandard-0.18.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7231543d38d2b7e02ef7cc78ef7ffd86419437e1114ff08709fe25a160e24bd6"}, + {file = "zstandard-0.18.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c86befac87445927488f5c8f205d11566f64c11519db223e9d282b945fa60dab"}, + {file = "zstandard-0.18.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:999a4e1768f219826ba3fa2064fab1c86dd72fdd47a42536235478c3bb3ca3e2"}, + {file = "zstandard-0.18.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df59cd1cf3c62075ee2a4da767089d19d874ac3ad42b04a71a167e91b384722"}, + {file = "zstandard-0.18.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1be31e9e3f7607ee0cdd60915410a5968b205d3e7aa83b7fcf3dd76dbbdb39e0"}, + {file = "zstandard-0.18.0-cp37-cp37m-win32.whl", hash = "sha256:490d11b705b8ae9dc845431bacc8dd1cef2408aede176620a5cd0cd411027936"}, + {file = "zstandard-0.18.0-cp37-cp37m-win_amd64.whl", hash = "sha256:266aba27fa9cc5e9091d3d325ebab1fa260f64e83e42516d5e73947c70216a5b"}, + {file = "zstandard-0.18.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b2260c4e07dd0723eadb586de7718b61acca4083a490dda69c5719d79bc715c"}, + {file = "zstandard-0.18.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3af8c2383d02feb6650e9255491ec7d0824f6e6dd2bbe3e521c469c985f31fb1"}, + {file = "zstandard-0.18.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28723a1d2e4df778573b76b321ebe9f3469ac98988104c2af116dd344802c3f8"}, + {file = "zstandard-0.18.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19cac7108ff2c342317fad6dc97604b47a41f403c8f19d0bfc396dfadc3638b8"}, + {file = "zstandard-0.18.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:76725d1ee83a8915100a310bbad5d9c1fc6397410259c94033b8318d548d9990"}, + {file = "zstandard-0.18.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d716a7694ce1fa60b20bc10f35c4a22be446ef7f514c8dbc8f858b61976de2fb"}, + {file = "zstandard-0.18.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:49685bf9a55d1ab34bd8423ea22db836ba43a181ac6b045ac4272093d5cb874e"}, + {file = "zstandard-0.18.0-cp38-cp38-win32.whl", hash = "sha256:1af1268a7dc870eb27515fb8db1f3e6c5a555d2b7bcc476fc3bab8886c7265ab"}, + {file = "zstandard-0.18.0-cp38-cp38-win_amd64.whl", hash = "sha256:1dc2d3809e763055a1a6c1a73f2b677320cc9a5aa1a7c6cfb35aee59bddc42d9"}, + {file = "zstandard-0.18.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eea18c1e7442f2aa9aff1bb84550dbb6a1f711faf6e48e7319de8f2b2e923c2a"}, + {file = "zstandard-0.18.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8677ffc6a6096cccbd892e558471c901fd821aba12b7fbc63833c7346f549224"}, + {file = "zstandard-0.18.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:083dc08abf03807af9beeb2b6a91c23ad78add2499f828176a3c7b742c44df02"}, + {file = "zstandard-0.18.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c990063664c08169c84474acecc9251ee035871589025cac47c060ff4ec4bc1a"}, + {file = "zstandard-0.18.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:533db8a6fac6248b2cb2c935e7b92f994efbdeb72e1ffa0b354432e087bb5a3e"}, + {file = "zstandard-0.18.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbb3cb8a082d62b8a73af42291569d266b05605e017a3d8a06a0e5c30b5f10f0"}, + {file = "zstandard-0.18.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d6c85ca5162049ede475b7ec98e87f9390501d44a3d6776ddd504e872464ec25"}, + {file = "zstandard-0.18.0-cp39-cp39-win32.whl", hash = "sha256:75479e7c2b3eebf402c59fbe57d21bc400cefa145ca356ee053b0a08908c5784"}, + {file = "zstandard-0.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:d85bfabad444812133a92fc6fbe463e1d07581dba72f041f07a360e63808b23c"}, + {file = "zstandard-0.18.0.tar.gz", hash = "sha256:0ac0357a0d985b4ff31a854744040d7b5754385d1f98f7145c30e02c6865cb6f"}, +] + +[package.dependencies] +cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\""} + +[package.extras] +cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.0" -python-versions = "<4.0,>=3.10" -content-hash = "7614f75bf08ad0550598113d72597e5a88323f917d800814e2db02a2c3a9c90b" +python-versions = "<3.11.9,>=3.10" +content-hash = "b7c2e9664977bb7296d12399542edd5cb05f2b16c82d6e2d86c751abf4217e7f" diff --git a/pyproject.toml b/pyproject.toml index 7d22e64..f41adc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ classifiers = [ "Environment :: Console", "Environment :: Web Environment", "Dev include = "packages" [tool.poetry.dependencies] -python = "<4.0,>=3.10" +python = "<3.11.9,>=3.10" open-autonomy = "==0.16.1" open-aea-test-autonomy = "==0.16.1" open-aea = "==1.57.0" @@ -27,10 +27,46 @@ black = "==24.2.0" isort = "==5.13.2" grpcio = "==1.53.0" asn1crypto = "<1.5.0,>=1.4.0" -open-aea-ledger-cosmos = "==1.57.0" py-ecc = "==6.0.0" pytz = "==2022.2.1" openapi-core = "==0.15.0" openapi-spec-validator = "<0.5.0,>=0.4.0" hypothesis = "==6.21.6" requests = "<2.31.2,>=2.28.1" +py-multibase = "==1.0.3" +py-multicodec = "==0.2.1" +py-eth-sig-utils = "*" +open-aea-ledger-cosmos = "==1.57.0" +open-aea-ledger-solana = "==1.57.0" +protobuf = "<4.25.0,>=4.21.6" +web3 = "<7,>=6.0.0" +ipfshttpclient = "==0.8.0a2" +open-aea-cli-ipfs = "==1.57.0" +aiohttp = "<4.0.0,>=3.8.5" +certifi = "*" +multidict = "*" +ecdsa = ">=0.15" +eth_typing = "*" +hexbytes = "==0.3.1" +packaging = "*" +websocket_client = "<1,>=0.32.0" +pytest-asyncio = "*" +eth-utils = "==2.2.0" +eth-abi = "==4.0.0" +pycryptodome = "==3.18.0" +pytest = "==7.2.1" +urllib3 = "==1.26.16" +jsonschema = "<4.4.0,>=4.3.0" +pandas = ">=1.3.0" +pyalgotrade = "==0.20" +lyra-v2-client = ">=0.2.9" +balpy = {path = "third_party/balpy"} +numpy = "==1.26.1" +dateparser = ">=1.1.1" +multicaller = {path = "third_party/multicaller"} + +[tool.poetry.group.dev.dependencies] +pytest-xprocess = ">=0.18.1,<0.19.0" +[tool.poetry.group.dev.dependencies.tomte] +version = ">=0.2.16" +extras = [ "cli", "tests",] From cc51ef53410ac4422b558f432ca91907c189a93c Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Tue, 22 Oct 2024 12:51:59 +0530 Subject: [PATCH 08/41] chore: update .gitigonre and .git modules --- .gitignore | 13 ------------- .gitmodules | 4 +--- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 859ba44..2c06345 100644 --- a/.gitignore +++ b/.gitignore @@ -20,12 +20,9 @@ packages/valory/contracts/multisend packages/valory/contracts/service_registry packages/valory/services/* packages/valory/protocols/* -!packages/valory/skills/* !packages/valory/agents/optimus !packages/valory/services/optimus -!packages/valory/skills/liquidity_trader_abci -!packages/valory/skills/optimus_abci leak_report /Pipfile.lock @@ -42,11 +39,6 @@ multisig_vault *private_key.txt* packages/fetchai/ packages/open_aea/ -packages/valory/connections/abci/ -packages/valory/connections/http_client/ -packages/valory/connections/ipfs/ -packages/valory/connections/ledger/ -packages/valory/connections/p2p_libp2p_client/ packages/valory/skills/abstract_abci/ packages/valory/skills/abstract_round_abci/ @@ -55,11 +47,6 @@ packages/valory/skills/reset_pause_abci/ packages/valory/skills/transaction_settlement_abci/ packages/valory/skills/termination_abci/ -packages/valory/contracts/multisend/ -packages/valory/contracts/service_registry/ -packages/valory/contracts/gnosis_safe/ -packages/valory/contracts/gnosis_safe_proxy_factory/ - packages/valory/protocols/abci packages/valory/protocols/acn packages/valory/protocols/contract_api diff --git a/.gitmodules b/.gitmodules index 0294686..ee2ecca 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,6 +4,4 @@ [submodule "third_party/multicaller"] path = third_party/multicaller url = https://github.com/8ball030/multicaller.git -[submodule "olas_arbitrage/aea_contracts_solana"] - path = olas_arbitrage/aea_contracts_solana - url = https://github.com/Dassy23/aea_contracts_solana.git + From f7151309689a4a1b22574ce4117d1a83f7f55b81 Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Tue, 22 Oct 2024 13:36:09 +0530 Subject: [PATCH 09/41] feat: merge dialogues and resolving import errors --- packages/packages.json | 8 +-- .../skills/liquidity_trader_abci/models.py | 2 +- .../skills/liquidity_trader_abci/rounds.py | 4 ++ .../valory/skills/optimus_abci/composition.py | 2 +- .../valory/skills/optimus_abci/dialogues.py | 26 ++++++++ packages/valory/skills/optimus_abci/models.py | 13 +++- scripts/aea-config-replace.py | 66 ++++++++++--------- 7 files changed, 83 insertions(+), 38 deletions(-) diff --git a/packages/packages.json b/packages/packages.json index 3bf13ca..02e1203 100644 --- a/packages/packages.json +++ b/packages/packages.json @@ -14,16 +14,16 @@ "contract/valory/staking_token/0.1.0": "bafybeifrvtkofw5c26b3irm6izqfdpik6vpjhm6hqwcdzx333h6vhdanai", "contract/valory/staking_activity_checker/0.1.0": "bafybeibjzsi2r5b6xd4iwl4wbwldptnynryzsdpifym4mkv32ynswx22ou", "skill/valory/liquidity_trader_abci/0.1.0": "bafybeibchfda234wvkiqayx7w2cnpxwfhmz3tct54cttp5ui6x65ppayzu", - "skill/valory/optimus_abci/0.1.0": "bafybeihfos26e7dwjhkomoh7vcvpaxzc6eyuaqgdeph3iadyxvztc2u42m", + "skill/valory/optimus_abci/0.1.0": "bafybeidrpmjnuu5xlfjktthv47rknpsrezmljlbtlrzvku5w34a2f2eakq", "skill/valory/trader_decision_maker_abci/0.1.0": "bafybeicskzcqr7dd4d5ulwss5u3hsx2wvcxatugtxiuja2neobbr3tqtkm", - "skill/valory/strategy_evaluator_abci/0.1.0": "bafybeidnozzuaeipbtapipeyupus7gmoq572pxp4sek36viyz3kbre2p54", + "skill/valory/strategy_evaluator_abci/0.1.0": "bafybeibdq4s53qakp55lagj6i2aut5vvqzidcvmo6bnqezie6ki7uzqnta", "skill/valory/market_data_fetcher_abci/0.1.0": "bafybeiffexwacktnhihmnhxdqs2msdzbigth62oqb7ghe2bqxwfkyvs5ty", "skill/valory/trader_abci/0.1.0": "bafybeig7vluejd62szp236nzhhgaaqbhcps2qgjhz2rmwjw2hijck2bfvm", "skill/valory/ipfs_package_downloader/0.1.0": "bafybeieokinjosnulrsee3kbj7ly4kjfx2ub6lmwkyplgs33vxgmx3fbvm", "skill/valory/portfolio_tracker_abci/0.1.0": "bafybeiay2lasy2mtp5fxd77wgn2ocgy5neg42mlyitltuzrr7b6yzbduoa", - "agent/valory/optimus/0.1.0": "bafybeigs3b7al7efb7gelmpizwbvqqwldjspahijxbrxbeipfp4tycenfe", + "agent/valory/optimus/0.1.0": "bafybeidsw5dfduvt4gcgaaaxkw2k43ivgceumjgody3f257wsrbdpe25mi", "agent/valory/solana_trader/0.1.0": "bafybeiei6g2i7bntofmpduy75tuulcleewmdiww5poxszj7yohv2wd63cq", - "service/valory/optimus/0.1.0": "bafybeidl3zdiml5silxvzxb63rxbt2rzfsyzt454wp3rexdonljdvbbfzy", + "service/valory/optimus/0.1.0": "bafybeiedmye7zypqrn6jrw6iyigwommbpdt5w6mvbvt7cgxk3otlntapli", "service/valory/solana_trader/0.1.0": "bafybeib5tasy5wc3cjbb6k42gz4gx3ub43cd67f66iay2fkxxcuxmnuqpy" }, "third_party": { diff --git a/packages/valory/skills/liquidity_trader_abci/models.py b/packages/valory/skills/liquidity_trader_abci/models.py index c46d656..880454d 100644 --- a/packages/valory/skills/liquidity_trader_abci/models.py +++ b/packages/valory/skills/liquidity_trader_abci/models.py @@ -255,7 +255,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: "min_swap_amount_threshold", kwargs, int ) self.agent_transition = self._ensure( - "merge_agent", kwargs, bool + "agent_transition", kwargs, bool ) self.max_fee_percentage = self._ensure("max_fee_percentage", kwargs, float) self.max_gas_percentage = self._ensure("max_gas_percentage", kwargs, float) diff --git a/packages/valory/skills/liquidity_trader_abci/rounds.py b/packages/valory/skills/liquidity_trader_abci/rounds.py index 6d7041d..7841886 100644 --- a/packages/valory/skills/liquidity_trader_abci/rounds.py +++ b/packages/valory/skills/liquidity_trader_abci/rounds.py @@ -41,6 +41,7 @@ EvaluateStrategyPayload, GetPositionsPayload, PostTxSettlementPayload, + DecideAgentPayload, ) @@ -539,6 +540,7 @@ class LiquidityTraderAbciApp(AbciApp[Event]): FinishedEvaluateStrategyRound: {}, FinishedTxPreparationRound: {}, FinishedDecisionMakingRound: {}, + SwitchAgentRound:{}, FinishedCallCheckpointRound: {}, FinishedCheckStakingKPIMetRound: {}, FailedMultiplexerRound: {}, @@ -546,6 +548,7 @@ class LiquidityTraderAbciApp(AbciApp[Event]): final_states: Set[AppState] = { FinishedEvaluateStrategyRound, FinishedDecisionMakingRound, + SwitchAgentRound, FinishedTxPreparationRound, FinishedCallCheckpointRound, FinishedCheckStakingKPIMetRound, @@ -577,5 +580,6 @@ class LiquidityTraderAbciApp(AbciApp[Event]): FailedMultiplexerRound: set(), FinishedEvaluateStrategyRound: set(), FinishedDecisionMakingRound: set(), + SwitchAgentRound: set(), FinishedTxPreparationRound: {get_name(SynchronizedData.most_voted_tx_hash)}, } diff --git a/packages/valory/skills/optimus_abci/composition.py b/packages/valory/skills/optimus_abci/composition.py index 00dad28..5647326 100644 --- a/packages/valory/skills/optimus_abci/composition.py +++ b/packages/valory/skills/optimus_abci/composition.py @@ -73,7 +73,7 @@ abci_app=TerminationAbciApp, ) -OptimusTraderAbciApp = chain( +OptimusAbciApp = chain( ( RegistrationAbci.AgentRegistrationAbciApp, TraderDecisionMakerAbci.TraderDecisionMakerAbciApp, diff --git a/packages/valory/skills/optimus_abci/dialogues.py b/packages/valory/skills/optimus_abci/dialogues.py index 77e9be1..91f58b5 100644 --- a/packages/valory/skills/optimus_abci/dialogues.py +++ b/packages/valory/skills/optimus_abci/dialogues.py @@ -18,6 +18,24 @@ # ------------------------------------------------------------------------------ """This module contains the dialogues of the OptimusAbciApp.""" +from packages.eightballer.protocols.balances.dialogues import ( + BalancesDialogue as BaseBalancesDialogue, +) +from packages.eightballer.protocols.balances.dialogues import ( + BalancesDialogues as BaseBalancesDialogues, +) +from packages.eightballer.protocols.orders.dialogues import ( + OrdersDialogue as BaseOrdersDialogue, +) +from packages.eightballer.protocols.orders.dialogues import ( + OrdersDialogues as BaseOrdersDialogues, +) +from packages.eightballer.protocols.tickers.dialogues import ( + TickersDialogue as BaseTickersDialogue, +) +from packages.eightballer.protocols.tickers.dialogues import ( + TickersDialogues as BaseTickersDialogues, +) from packages.valory.skills.abstract_round_abci.dialogues import ( AbciDialogue as BaseAbciDialogue, @@ -86,6 +104,14 @@ TendermintDialogue = BaseTendermintDialogue TendermintDialogues = BaseTendermintDialogues +TickersDialogue = BaseTickersDialogue +TickersDialogues = BaseTickersDialogues IpfsDialogue = BaseIpfsDialogue IpfsDialogues = BaseIpfsDialogues + +BalancesDialogue = BaseBalancesDialogue +BalancesDialogues = BaseBalancesDialogues + +OrdersDialogue = BaseOrdersDialogue +OrdersDialogues = BaseOrdersDialogues \ No newline at end of file diff --git a/packages/valory/skills/optimus_abci/models.py b/packages/valory/skills/optimus_abci/models.py index 5f3b152..6daaefc 100644 --- a/packages/valory/skills/optimus_abci/models.py +++ b/packages/valory/skills/optimus_abci/models.py @@ -44,7 +44,13 @@ from packages.valory.skills.transaction_settlement_abci.rounds import ( Event as TransactionSettlementEvent, ) - +from packages.valory.skills.portfolio_tracker_abci.models import GetBalance +from packages.valory.skills.portfolio_tracker_abci.models import TokenAccounts +from packages.valory.skills.strategy_evaluator_abci.models import ( + SwapInstructionsSpecs, + SwapQuotesSpecs, + TxSettlementProxy, +) EventType = Union[ Type[LiquidityTraderEvent], @@ -59,6 +65,11 @@ Coingecko = Coingecko Requests = BaseRequests BenchmarkTool = BaseBenchmarkTool +SwapQuotesSpecs = SwapQuotesSpecs +SwapInstructionsSpecs = SwapInstructionsSpecs +TxSettlementProxy = TxSettlementProxy +GetBalance = GetBalance +TokenAccounts = TokenAccounts RandomnessApi = BaseRandomnessApi diff --git a/scripts/aea-config-replace.py b/scripts/aea-config-replace.py index 5b3bcfc..9516e50 100644 --- a/scripts/aea-config-replace.py +++ b/scripts/aea-config-replace.py @@ -32,6 +32,7 @@ def main() -> None: load_dotenv() with open(Path("optimus", "aea-config.yaml"), "r", encoding="utf-8") as file: + print("config path",Path("optimus", "aea-config.yaml")) config = list(yaml.safe_load_all(file)) # Ledger RPCs @@ -51,37 +52,40 @@ def main() -> None: ] = f"${{str:{os.getenv('OPTIMISM_LEDGER_RPC')}}}" # Params - config[5]["models"]["params"]["args"]["setup"][ - "all_participants" - ] = f"${{list:{os.getenv('ALL_PARTICIPANTS')}}}" - - config[5]["models"]["params"]["args"][ - "safe_contract_addresses" - ] = f"${{str:{os.getenv('SAFE_CONTRACT_ADDRESSES')}}}" - - config[5]["models"]["params"]["args"][ - "slippage_for_swap" - ] = f"${{float:{os.getenv('SLIPPAGE_FOR_SWAP')}}}" - - config[5]["models"]["params"]["args"][ - "tenderly_access_key" - ] = f"${{str:{os.getenv('TENDERLY_ACCESS_KEY')}}}" - - config[5]["models"]["params"]["args"][ - "agent_transition" - ] = f"${{str:{os.getenv('AGENT_TRANSITION')}}}" - - config[5]["models"]["params"]["args"][ - "tenderly_account_slug" - ] = f"${{str:{os.getenv('TENDERLY_ACCOUNT_SLUG')}}}" - - config[5]["models"]["params"]["args"][ - "tenderly_project_slug" - ] = f"${{str:{os.getenv('TENDERLY_PROJECT_SLUG')}}}" - - config[5]["models"]["coingecko"]["args"][ - "api_key" - ] = f"${{str:{os.getenv('COINGECKO_API_KEY')}}}" + try: + config[5]["models"]["params"]["args"]["setup"][ + "all_participants" + ] = f"${{list:{os.getenv('ALL_PARTICIPANTS')}}}" + + config[5]["models"]["params"]["args"][ + "safe_contract_addresses" + ] = f"${{str:{os.getenv('SAFE_CONTRACT_ADDRESSES')}}}" + + config[5]["models"]["params"]["args"][ + "slippage_for_swap" + ] = f"${{float:{os.getenv('SLIPPAGE_FOR_SWAP')}}}" + + config[5]["models"]["params"]["args"][ + "tenderly_access_key" + ] = f"${{str:{os.getenv('TENDERLY_ACCESS_KEY')}}}" + + config[5]["models"]["params"]["args"][ + "agent_transition" + ] = f"${{bool:{os.getenv('AGENT_TRANSITION')}}}" + + config[5]["models"]["params"]["args"][ + "tenderly_account_slug" + ] = f"${{str:{os.getenv('TENDERLY_ACCOUNT_SLUG')}}}" + + config[5]["models"]["params"]["args"][ + "tenderly_project_slug" + ] = f"${{str:{os.getenv('TENDERLY_PROJECT_SLUG')}}}" + + config[5]["models"]["coingecko"]["args"][ + "api_key" + ] = f"${{str:{os.getenv('COINGECKO_API_KEY')}}}" + except KeyError as e: + print("Error", e) with open(Path("optimus", "aea-config.yaml"), "w", encoding="utf-8") as file: yaml.dump_all(config, file, sort_keys=False) From 839c7d5796378aaff8f82dcd18872a3ef9a03b5a Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Tue, 22 Oct 2024 14:27:16 +0530 Subject: [PATCH 10/41] chore: Update event values in LiquidityTraderAbciApp and event values in DecideAgentBehaviour --- packages/valory/skills/liquidity_trader_abci/behaviours.py | 4 ++-- packages/valory/skills/liquidity_trader_abci/rounds.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/valory/skills/liquidity_trader_abci/behaviours.py b/packages/valory/skills/liquidity_trader_abci/behaviours.py index 9a53fa7..07cfe10 100644 --- a/packages/valory/skills/liquidity_trader_abci/behaviours.py +++ b/packages/valory/skills/liquidity_trader_abci/behaviours.py @@ -3366,9 +3366,9 @@ def async_act(self) -> Generator: with self.context.benchmark_tool.measure(self.behaviour_id).local(): sender = self.context.agent_address if self.params.agent_transition is True: - next_event = Event.MOVE_TO_NEXT_AGENT + next_event = Event.MOVE_TO_NEXT_AGENT.value else: - next_event = Event.DONT_MOVE_TO_NEXT_AGENT + next_event = Event.DONT_MOVE_TO_NEXT_AGENT.value payload = DecideAgentPayload( sender=sender, diff --git a/packages/valory/skills/liquidity_trader_abci/rounds.py b/packages/valory/skills/liquidity_trader_abci/rounds.py index 7841886..5da35d5 100644 --- a/packages/valory/skills/liquidity_trader_abci/rounds.py +++ b/packages/valory/skills/liquidity_trader_abci/rounds.py @@ -513,7 +513,7 @@ class LiquidityTraderAbciApp(AbciApp[Event]): Event.DONE: DecisionMakingRound, Event.NO_MAJORITY: EvaluateStrategyRound, Event.ROUND_TIMEOUT: EvaluateStrategyRound, - Event.WAIT: FinishedEvaluateStrategyRound, + Event.WAIT: DecideAgentRound, }, DecisionMakingRound: { Event.DONE: DecideAgentRound, From c83ab5d40745c4acef2658cfb9b2cfe8df30ba38 Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Tue, 22 Oct 2024 19:12:10 +0530 Subject: [PATCH 11/41] chore: remove skills --- .../agents/optimus/aea-config-optimus.yaml | 267 -- .../valory/skills/abstract_abci/README.md | 20 - .../valory/skills/abstract_abci/__init__.py | 25 - .../valory/skills/abstract_abci/dialogues.py | 61 - .../valory/skills/abstract_abci/handlers.py | 408 -- .../valory/skills/abstract_abci/skill.yaml | 34 - .../skills/abstract_abci/tests/__init__.py | 20 - .../abstract_abci/tests/test_dialogues.py | 47 - .../abstract_abci/tests/test_handlers.py | 398 -- .../skills/abstract_round_abci/README.md | 46 - .../skills/abstract_round_abci/__init__.py | 25 - .../abstract_round_abci/abci_app_chain.py | 291 -- .../valory/skills/abstract_round_abci/base.py | 3850 ----------------- .../abstract_round_abci/behaviour_utils.py | 2356 ---------- .../skills/abstract_round_abci/behaviours.py | 409 -- .../skills/abstract_round_abci/common.py | 231 - .../skills/abstract_round_abci/dialogues.py | 368 -- .../skills/abstract_round_abci/handlers.py | 790 ---- .../abstract_round_abci/io_/__init__.py | 20 - .../skills/abstract_round_abci/io_/ipfs.py | 85 - .../skills/abstract_round_abci/io_/load.py | 124 - .../skills/abstract_round_abci/io_/paths.py | 34 - .../skills/abstract_round_abci/io_/store.py | 153 - .../skills/abstract_round_abci/models.py | 893 ---- .../skills/abstract_round_abci/skill.yaml | 164 - .../test_tools/__init__.py | 20 - .../test_tools/abci_app.py | 203 - .../abstract_round_abci/test_tools/base.py | 444 -- .../abstract_round_abci/test_tools/common.py | 432 -- .../test_tools/integration.py | 301 -- .../abstract_round_abci/test_tools/rounds.py | 599 --- .../abstract_round_abci/tests/__init__.py | 27 - .../abstract_round_abci/tests/conftest.py | 116 - .../tests/data/__init__.py | 20 - .../tests/test_abci_app_chain.py | 508 --- .../abstract_round_abci/tests/test_base.py | 3420 --------------- .../tests/test_base_rounds.py | 668 --- .../tests/test_behaviours.py | 951 ---- .../tests/test_behaviours_utils.py | 2681 ------------ .../abstract_round_abci/tests/test_common.py | 407 -- .../tests/test_dialogues.py | 100 - .../tests/test_handlers.py | 596 --- .../abstract_round_abci/tests/test_models.py | 901 ---- .../abstract_round_abci/tests/test_utils.py | 307 -- .../skills/abstract_round_abci/utils.py | 504 --- 45 files changed, 24324 deletions(-) delete mode 100644 packages/valory/agents/optimus/aea-config-optimus.yaml delete mode 100644 packages/valory/skills/abstract_abci/README.md delete mode 100644 packages/valory/skills/abstract_abci/__init__.py delete mode 100644 packages/valory/skills/abstract_abci/dialogues.py delete mode 100644 packages/valory/skills/abstract_abci/handlers.py delete mode 100644 packages/valory/skills/abstract_abci/skill.yaml delete mode 100644 packages/valory/skills/abstract_abci/tests/__init__.py delete mode 100644 packages/valory/skills/abstract_abci/tests/test_dialogues.py delete mode 100644 packages/valory/skills/abstract_abci/tests/test_handlers.py delete mode 100644 packages/valory/skills/abstract_round_abci/README.md delete mode 100644 packages/valory/skills/abstract_round_abci/__init__.py delete mode 100644 packages/valory/skills/abstract_round_abci/abci_app_chain.py delete mode 100644 packages/valory/skills/abstract_round_abci/base.py delete mode 100644 packages/valory/skills/abstract_round_abci/behaviour_utils.py delete mode 100644 packages/valory/skills/abstract_round_abci/behaviours.py delete mode 100644 packages/valory/skills/abstract_round_abci/common.py delete mode 100644 packages/valory/skills/abstract_round_abci/dialogues.py delete mode 100644 packages/valory/skills/abstract_round_abci/handlers.py delete mode 100644 packages/valory/skills/abstract_round_abci/io_/__init__.py delete mode 100644 packages/valory/skills/abstract_round_abci/io_/ipfs.py delete mode 100644 packages/valory/skills/abstract_round_abci/io_/load.py delete mode 100644 packages/valory/skills/abstract_round_abci/io_/paths.py delete mode 100644 packages/valory/skills/abstract_round_abci/io_/store.py delete mode 100644 packages/valory/skills/abstract_round_abci/models.py delete mode 100644 packages/valory/skills/abstract_round_abci/skill.yaml delete mode 100644 packages/valory/skills/abstract_round_abci/test_tools/__init__.py delete mode 100644 packages/valory/skills/abstract_round_abci/test_tools/abci_app.py delete mode 100644 packages/valory/skills/abstract_round_abci/test_tools/base.py delete mode 100644 packages/valory/skills/abstract_round_abci/test_tools/common.py delete mode 100644 packages/valory/skills/abstract_round_abci/test_tools/integration.py delete mode 100644 packages/valory/skills/abstract_round_abci/test_tools/rounds.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/__init__.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/conftest.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/data/__init__.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/test_abci_app_chain.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/test_base.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/test_base_rounds.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/test_behaviours.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/test_behaviours_utils.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/test_common.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/test_dialogues.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/test_handlers.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/test_models.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/test_utils.py delete mode 100644 packages/valory/skills/abstract_round_abci/utils.py diff --git a/packages/valory/agents/optimus/aea-config-optimus.yaml b/packages/valory/agents/optimus/aea-config-optimus.yaml deleted file mode 100644 index bdff5a3..0000000 --- a/packages/valory/agents/optimus/aea-config-optimus.yaml +++ /dev/null @@ -1,267 +0,0 @@ -agent_name: solana_trader -author: valory -version: 0.1.0 -license: Apache-2.0 -description: Solana trader agent. -aea_version: '>=1.0.0, <2.0.0' -fingerprint: - __init__.py: bafybeigx5mdvnamsqfum5ut7htok2y5vsnu7lrvms5gfvqi7hmv7sfbo3a - README.md: bafybeibm2adzlongvgzyepiiymb3hxpsjb43qgr7j4uydebjzcpdrwm3om -fingerprint_ignore_patterns: [] -connections: -- eightballer/dcxt:0.1.0:bafybeihes2id6qmqqybvdqfpchu6h3i5mpnhq6lztfxp52twd22iwqxacq -- valory/abci:0.1.0:bafybeiejymu4ul62zx6weoibnlsrfprfpjnplhjefz6sr6izgdr4sajlnu -- valory/http_client:0.23.0:bafybeihi772xgzpqeipp3fhmvpct4y6e6tpjp4sogwqrnf3wqspgeilg4u -- valory/http_server:0.22.0:bafybeihpgu56ovmq4npazdbh6y6ru5i7zuv6wvdglpxavsckyih56smu7m -- valory/ipfs:0.1.0:bafybeiegnapkvkamis47v5ioza2haerrjdzzb23rptpmcydyneas7jc2wm -- valory/ledger:0.19.0:bafybeigntoericenpzvwejqfuc3kqzo2pscs76qoygg5dbj6f4zxusru5e -- valory/p2p_libp2p_client:0.1.0:bafybeid3xg5k2ol5adflqloy75ibgljmol6xsvzvezebsg7oudxeeolz7e -contracts: -- eightballer/erc_20:0.1.0:bafybeiezbnm3f5zhuj5bsc542isnlh2fki5q4nmm2vsajzps4uuoamofo4 -- valory/multisend:0.1.0:bafybeig5byt5urg2d2bsecufxe5ql7f4mezg3mekfleeh32nmuusx66p4y -- valory/service_registry:0.1.0:bafybeihafe524ilngwzavkhwz4er56p7nyar26lfm7lrksfiqvvzo3kdcq -- valory/gnosis_safe:0.1.0:bafybeiho6sbfts3zk3mftrngw37d5qnlvkqtnttt3fzexmcwkeevhu4wwi -- valory/gnosis_safe_proxy_factory:0.1.0:bafybeicpcpyurm7gxir2gnlsgzeirzomkhcbnzr5txk67zdf4mmg737rtu -- valory/balancer_weighted_pool:0.1.0:bafybeidyjlrlq3jrbackewedwt5irokhjupxgpqfgur2ri426cap2oqt7a -- valory/balancer_vault:0.1.0:bafybeie6twptrkqddget7pjijzob2c4jqmrrtpkwombneh35xx56djz4ru -- valory/uniswap_v3_non_fungible_position_manager:0.1.0:bafybeigadr3nyx6tkrual7oqn2qiup35addfevromxjzzlvkiukpyhtz6y -- valory/uniswap_v3_pool:0.1.0:bafybeih64nqgwlverl2tubnkymtlvewngn2pthzzfjewvxpk7dt2im6gza -protocols: -- open_aea/signing:1.0.0:bafybeihv62fim3wl2bayavfcg3u5e5cxu3b7brtu4cn5xoxd6lqwachasi -- valory/abci:0.1.0:bafybeiaqmp7kocbfdboksayeqhkbrynvlfzsx4uy4x6nohywnmaig4an7u -- valory/acn:1.1.0:bafybeidluaoeakae3exseupaea4i3yvvk5vivyt227xshjlffywwxzcxqe -- valory/contract_api:1.0.0:bafybeidgu7o5llh26xp3u3ebq3yluull5lupiyeu6iooi2xyymdrgnzq5i -- valory/http:1.0.0:bafybeifugzl63kfdmwrxwphrnrhj7bn6iruxieme3a4ntzejf6kmtuwmae -- valory/ipfs:0.1.0:bafybeiftxi2qhreewgsc5wevogi7yc5g6hbcbo4uiuaibauhv3nhfcdtvm -- valory/ledger_api:1.0.0:bafybeihdk6psr4guxmbcrc26jr2cbgzpd5aljkqvpwo64bvaz7tdti2oni -- valory/tendermint:0.1.0:bafybeig4mi3vmlv5zpbjbfuzcgida6j5f2nhrpedxicmrrfjweqc5r7cra -skills: -- valory/abstract_abci:0.1.0:bafybeidz54kvxhbdmpruzguuzzq7bjg4pekjb5amqobkxoy4oqknnobopu -- valory/abstract_round_abci:0.1.0:bafybeiajjzuh6vf23crp55humonknirvv2f4s3dmdlfzch6tc5ow52pcgm -- valory/liquidity_trader_abci:0.1.0:bafybeihtca6gtyjibj6wkrcdmx3fb3a3bkpdgsphwevkatagxrbqvh6fd4 -- valory/optimus_abci:0.1.0:bafybeifjpvqz2m7qhztib4xcjpbjkuiutrot22flqclg36amvqvrp5ra3e -- valory/registration_abci:0.1.0:bafybeiffipsowrqrkhjoexem7ern5ob4fabgif7wa6gtlszcoaop2e3oey -- valory/reset_pause_abci:0.1.0:bafybeif4lgvbzsmzljesxbphycdv52ka7qnihyjrjpfaseclxadcmm6yiq -- valory/termination_abci:0.1.0:bafybeiekkpo5qef5zaeagm3si6v45qxcojvtjqe4a5ceccvk4q7k3xi3bi -- valory/transaction_settlement_abci:0.1.0:bafybeielv6eivt2z6nforq43xewl2vmpfwpdu2s2vfogobziljnwsclmlm -- valory/market_data_fetcher_abci:0.1.0:bafybeia3kld7ogbaolbxskys7r5ccolhm53fqi4tdkrwnvilfm7gn5ztcm -- valory/strategy_evaluator_abci:0.1.0:bafybeig2mx3abjgjhiizx2kez3462mcygjwlqj5d6jvnovcf7rwzaql43e -- valory/trader_abci:0.1.0:bafybeiccni66lhpc6nt4hirtakw3xzhera7kqkzmreuznrpuxpckuh455e -- valory/trader_decision_maker_abci:0.1.0:bafybeih7duxvbjevmeez4bvdpahgvefoik3hnpjq3ik5g4jaqbuw5rybtu -- valory/ipfs_package_downloader:0.1.0:bafybeid54srronvfqbvcdjgtuhmr4mbndjkpxtgzguykeg4p3wwj3zboyi -- valory/portfolio_tracker_abci:0.1.0:bafybeigzyhm3fzoxhggjdexryzqgskafoi6rec4ois34n3asodxn6j3txm -default_ledger: ethereum -required_ledgers: -- ethereum -- solana -default_routing: {} -connection_private_key_paths: {} -private_key_paths: {} -logging_config: - version: 1 - disable_existing_loggers: false - formatters: - standard: - format: '[%(asctime)s] [%(levelname)s] %(message)s' - handlers: - logfile: - class: logging.FileHandler - formatter: standard - filename: ${LOG_FILE:str:log.txt} - level: ${LOG_LEVEL:str:INFO} - console: - class: logging.StreamHandler - formatter: standard - stream: ext://sys.stdout - loggers: - aea: - handlers: - - logfile - - console - propagate: true -dependencies: - open-aea-ledger-cosmos: - version: ==1.55.0 - open-aea-ledger-solana: - version: ==1.55.0 - open-aea-ledger-ethereum: - version: ==1.55.0 - open-aea-test-autonomy: - version: ==0.15.2 - pyalgotrade: - version: ==0.20 - open-aea-ledger-ethereum-tool: - version: ==1.57.0 -skill_exception_policy: stop_and_exit -connection_exception_policy: just_log -default_connection: null ---- -public_id: valory/abci:0.1.0 -type: connection -config: - target_skill_id: valory/trader_abci:0.1.0 - host: ${str:localhost} - port: ${int:26658} - use_tendermint: ${bool:false} ---- -public_id: valory/ledger:0.19.0 -type: connection -config: - ledger_apis: - ethereum: - address: ${str:https://base.blockpi.network/v1/rpc/public} - chain_id: ${int:8453} - poa_chain: ${bool:false} - default_gas_price_strategy: ${str:eip1559} - base: - address: ${str:https://virtual.base.rpc.tenderly.co/5d9c013b-879b-4f20-a6cc-e95dee0d109f} - chain_id: ${int:8453} - poa_chain: ${bool:false} - default_gas_price_strategy: ${str:eip1559} - optimism: - address: ${str:https://mainnet.optimism.io} - chain_id: ${int:10} - poa_chain: ${bool:false} - default_gas_price_strategy: ${str:eip1559} ---- -public_id: valory/p2p_libp2p_client:0.1.0 -type: connection -config: - nodes: - - uri: ${str:acn.staging.autonolas.tech:9005} - public_key: ${str:02d3a830c9d6ea1ae91936951430dee11f4662f33118b02190693be835359a9d77} -cert_requests: -- identifier: acn - ledger_id: ethereum - message_format: '{public_key}' - not_after: '2024-01-01' - not_before: '2023-01-01' - public_key: ${str:02d3a830c9d6ea1ae91936951430dee11f4662f33118b02190693be835359a9d77} - save_path: .certs/acn_cosmos_9005.txt ---- -public_id: valory/http_server:0.22.0:bafybeicblltx7ha3ulthg7bzfccuqqyjmihhrvfeztlgrlcoxhr7kf6nbq -type: connection -config: - host: 0.0.0.0 - target_skill_id: valory/trader_abci:0.1.0 ---- -public_id: valory/http_client:0.23.0 -type: connection -config: - host: ${str:127.0.0.1} - port: ${int:8000} - timeout: ${int:1200} ---- -public_id: eightballer/dcxt:0.1.0 -type: connection -config: - target_skill_id: eightballer/chained_dex_app:0.1.0 - exchanges: - - name: ${str:balancer} - key_path: ${str:ethereum_private_key.txt} - ledger_id: ${str:base} - rpc_url: ${str:https://base.blockpi.network/v1/rpc/public} - etherscan_api_key: ${str:YOUR_ETHERSCAN_API_KEY} ---- -public_id: valory/ipfs_package_downloader:0.1.0 -type: skill -models: - params: - args: - cleanup_freq: ${int:50} - timeout_limit: ${int:3} - file_hash_to_id: ${list:[["bafybeic2fpf5ozhkf5jgzmppmfsprqw5ayfx6spgl3owuws464n7mkhpqi",["sma_strategy"]]]} - component_yaml_filename: ${str:component.yaml} - entry_point_key: ${str:entry_point} - callable_keys: ${list:["run_callable","transform_callable","evaluate_callable"]} ---- -public_id: valory/trader_abci:0.1.0 -type: skill -models: - benchmark_tool: - args: - log_dir: ${str:/benchmarks} - get_balance: - args: - api_id: ${str:get_balance} - headers: - Content-Type: ${str:application/json} - method: ${str:POST} - parameters: ${dict:{}} - response_key: ${str:result:value} - response_type: ${str:int} - error_key: ${str:error:message} - error_type: ${str:str} - retries: ${int:5} - url: ${str:https://api.mainnet-beta.solana.com} - token_accounts: - args: - api_id: ${str:token_accounts} - headers: - Content-Type: ${str:application/json} - method: ${str:POST} - parameters: ${dict:{}} - response_key: ${str:result:value} - response_type: ${str:list} - error_key: ${str:error:message} - error_type: ${str:str} - retries: ${int:5} - url: ${str:https://api.mainnet-beta.solana.com} - coingecko: - args: - token_price_endpoint: ${str:https://api.coingecko.com/api/v3/simple/token_price/{asset_platform_id}?contract_addresses={token_address}&vs_currencies=usd} - coin_price_endpoint: ${str:https://api.coingecko.com/api/v3/coins/{token_id}/market_chart?vs_currency=usd&days=1} - api_key: ${str:null} - prices_field: ${str:prices} - requests_per_minute: ${int:5} - credits: ${int:10000} - rate_limited_code: ${int:429} - tx_settlement_proxy: - args: - api_id: ${str:tx_settlement_proxy} - headers: - Content-Type: ${str:application/json} - method: ${str:POST} - parameters: - amount: ${int:100000000} - slippageBps: ${int:5} - resendAmount: ${int:200} - timeoutInMs: ${int:120000} - priorityFee: ${int:5000000} - response_key: ${str:null} - response_type: ${str:dict} - retries: ${int:5} - url: ${str:http://localhost:3000/tx} - params: - args: - setup: - all_participants: ${list:["0x0000000000000000000000000000000000000000"]} - consensus_threshold: ${int:null} - safe_contract_address: ${str:0x0000000000000000000000000000000000000000} - cleanup_history_depth: ${int:1} - cleanup_history_depth_current: ${int:null} - drand_public_key: ${str:868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31} - genesis_config: - genesis_time: ${str:2022-09-26T00:00:00.000000000Z} - chain_id: ${str:chain-c4daS1} - consensus_params: - block: - max_bytes: ${str:22020096} - max_gas: ${str:-1} - time_iota_ms: ${str:1000} - evidence: - max_age_num_blocks: ${str:100000} - max_age_duration: ${str:172800000000000} - max_bytes: ${str:1048576} - validator: - pub_key_types: ${list:["ed25519"]} - version: ${dict:{}} - voting_power: ${str:10} - init_fallback_gas: ${int:0} - keeper_allowed_retries: ${int:3} - keeper_timeout: ${float:30.0} - max_attempts: ${int:10} - reset_tendermint_after: ${int:2} - retry_attempts: ${int:400} - retry_timeout: ${int:3} - request \ No newline at end of file diff --git a/packages/valory/skills/abstract_abci/README.md b/packages/valory/skills/abstract_abci/README.md deleted file mode 100644 index 4a99a11..0000000 --- a/packages/valory/skills/abstract_abci/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# Abstract abci - -## Description - -This module contains an abstract ABCI skill template for an AEA. - -## Behaviours - -No behaviours (the skill is purely reactive). - -## Handlers - -* `ABCIHandler` - - This abstract skill provides a template of an ABCI application managed by an - AEA. This abstract Handler replies to ABCI requests with default responses. - In another skill, extend the class and override the request handlers - to implement a custom behaviour. - - diff --git a/packages/valory/skills/abstract_abci/__init__.py b/packages/valory/skills/abstract_abci/__init__.py deleted file mode 100644 index 8c9d135..0000000 --- a/packages/valory/skills/abstract_abci/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains an abstract ABCI skill template for an AEA.""" - -from aea.configurations.base import PublicId - - -PUBLIC_ID = PublicId.from_str("valory/abstract_abci:0.1.0") diff --git a/packages/valory/skills/abstract_abci/dialogues.py b/packages/valory/skills/abstract_abci/dialogues.py deleted file mode 100644 index e38665b..0000000 --- a/packages/valory/skills/abstract_abci/dialogues.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the classes required for dialogue management.""" - -from typing import Any - -from aea.protocols.base import Address, Message -from aea.protocols.dialogue.base import Dialogue as BaseDialogue -from aea.skills.base import Model - -from packages.valory.protocols.abci.dialogues import AbciDialogue as BaseAbciDialogue -from packages.valory.protocols.abci.dialogues import AbciDialogues as BaseAbciDialogues - - -AbciDialogue = BaseAbciDialogue - - -class AbciDialogues(Model, BaseAbciDialogues): - """The dialogues class keeps track of all dialogues.""" - - def __init__(self, **kwargs: Any) -> None: - """ - Initialize dialogues. - - :param kwargs: keyword arguments - """ - Model.__init__(self, **kwargs) - - def role_from_first_message( # pylint: disable=unused-argument - message: Message, receiver_address: Address - ) -> BaseDialogue.Role: - """Infer the role of the agent from an incoming/outgoing first message - - :param message: an incoming/outgoing first message - :param receiver_address: the address of the receiving agent - :return: The role of the agent - """ - return AbciDialogue.Role.CLIENT - - BaseAbciDialogues.__init__( - self, - self_address=str(self.skill_id), - role_from_first_message=role_from_first_message, - ) diff --git a/packages/valory/skills/abstract_abci/handlers.py b/packages/valory/skills/abstract_abci/handlers.py deleted file mode 100644 index 93e95e3..0000000 --- a/packages/valory/skills/abstract_abci/handlers.py +++ /dev/null @@ -1,408 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the handler for the 'abci' skill.""" -from typing import List, cast - -from aea.protocols.base import Message -from aea.skills.base import Handler - -from packages.valory.connections.abci.connection import PUBLIC_ID -from packages.valory.protocols.abci import AbciMessage -from packages.valory.protocols.abci.custom_types import ( - Events, - ProofOps, - Result, - ResultType, - SnapShots, - ValidatorUpdates, -) -from packages.valory.protocols.abci.dialogues import AbciDialogue, AbciDialogues - - -ERROR_CODE = 1 - - -class ABCIHandler(Handler): - """ - Default ABCI handler. - - This abstract skill provides a template of an ABCI application managed by an - AEA. This abstract Handler replies to ABCI requests with default responses. - In another skill, extend the class and override the request handlers - to implement a custom behaviour. - """ - - SUPPORTED_PROTOCOL = AbciMessage.protocol_id - - def setup(self) -> None: - """Set up the handler.""" - self.context.logger.debug( - f"ABCI Handler: setup method called. Using {PUBLIC_ID}." - ) - - def handle(self, message: Message) -> None: - """ - Handle the message. - - :param message: the message. - """ - abci_message = cast(AbciMessage, message) - - # recover dialogue - abci_dialogues = cast(AbciDialogues, self.context.abci_dialogues) - abci_dialogue = cast(AbciDialogue, abci_dialogues.update(message)) - - if abci_dialogue is None: - self.log_exception(abci_message, "Invalid dialogue.") - return - - performative = message.performative.value - - # handle message - request_type = performative.replace("request_", "") - self.context.logger.debug(f"Received ABCI request of type {request_type}") - handler = getattr(self, request_type, None) - if handler is None: # pragma: nocover - self.context.logger.warning( - f"Cannot handle request '{request_type}', ignoring..." - ) - return - - self.context.logger.debug( - "ABCI Handler: message={}, sender={}".format(message, message.sender) - ) - response = handler(message, abci_dialogue) - self.context.outbox.put_message(message=response) - - def teardown(self) -> None: - """Teardown the handler.""" - self.context.logger.debug("ABCI Handler: teardown method called.") - - def log_exception(self, message: AbciMessage, error_message: str) -> None: - """Log a response exception.""" - self.context.logger.error( - f"An exception occurred: {error_message} for message: {message}" - ) - - def echo( # pylint: disable=no-self-use - self, message: AbciMessage, dialogue: AbciDialogue - ) -> AbciMessage: - """ - Handle a message of REQUEST_ECHO performative. - - :param message: the ABCI request. - :param dialogue: the ABCI dialogue. - :return: the response. - """ - reply = dialogue.reply( - performative=AbciMessage.Performative.RESPONSE_ECHO, - target_message=message, - message=message.message, - ) - return cast(AbciMessage, reply) - - def info( # pylint: disable=no-self-use - self, message: AbciMessage, dialogue: AbciDialogue - ) -> AbciMessage: - """ - Handle a message of REQUEST_INFO performative. - - :param message: the ABCI request. - :param dialogue: the ABCI dialogue. - :return: the response. - """ - info_data = "" - version = "" - app_version = 0 - last_block_height = 0 - last_block_app_hash = b"" - reply = dialogue.reply( - performative=AbciMessage.Performative.RESPONSE_INFO, - target_message=message, - info_data=info_data, - version=version, - app_version=app_version, - last_block_height=last_block_height, - last_block_app_hash=last_block_app_hash, - ) - return cast(AbciMessage, reply) - - def flush( # pylint: disable=no-self-use - self, - message: AbciMessage, - dialogue: AbciDialogue, - ) -> AbciMessage: - """ - Handle a message of REQUEST_FLUSH performative. - - :param message: the ABCI request. - :param dialogue: the ABCI dialogue. - :return: the response. - """ - reply = dialogue.reply( - performative=AbciMessage.Performative.RESPONSE_FLUSH, - target_message=message, - ) - return cast(AbciMessage, reply) - - def set_option( # pylint: disable=no-self-use - self, - message: AbciMessage, - dialogue: AbciDialogue, - ) -> AbciMessage: - """ - Handle a message of REQUEST_SET_OPTION performative. - - :param message: the ABCI request. - :param dialogue: the ABCI dialogue. - :return: the response. - """ - reply = dialogue.reply( - performative=AbciMessage.Performative.RESPONSE_SET_OPTION, - target_message=message, - code=ERROR_CODE, - log="operation not supported", - info="operation not supported", - ) - return cast(AbciMessage, reply) - - def init_chain( # pylint: disable=no-self-use - self, message: AbciMessage, dialogue: AbciDialogue - ) -> AbciMessage: - """ - Handle a message of REQUEST_INIT_CHAIN performative. - - :param message: the ABCI request. - :param dialogue: the ABCI dialogue. - :return: the response. - """ - validators: List = [] - app_hash = b"" - reply = dialogue.reply( - performative=AbciMessage.Performative.RESPONSE_INIT_CHAIN, - target_message=message, - validators=ValidatorUpdates(validators), - app_hash=app_hash, - ) - return cast(AbciMessage, reply) - - def query( # pylint: disable=no-self-use - self, message: AbciMessage, dialogue: AbciDialogue - ) -> AbciMessage: - """ - Handle a message of REQUEST_QUERY performative. - - :param message: the ABCI request. - :param dialogue: the ABCI dialogue. - :return: the response. - """ - reply = dialogue.reply( - performative=AbciMessage.Performative.RESPONSE_QUERY, - target_message=message, - code=ERROR_CODE, - log="operation not supported", - info="operation not supported", - index=0, - key=b"", - value=b"", - proof_ops=ProofOps([]), - height=0, - codespace="", - ) - return cast(AbciMessage, reply) - - def check_tx( # pylint: disable=no-self-use - self, message: AbciMessage, dialogue: AbciDialogue - ) -> AbciMessage: - """ - Handle a message of REQUEST_CHECK_TX performative. - - :param message: the ABCI request. - :param dialogue: the ABCI dialogue. - :return: the response. - """ - reply = dialogue.reply( - performative=AbciMessage.Performative.RESPONSE_CHECK_TX, - target_message=message, - code=ERROR_CODE, - data=b"", - log="operation not supported", - info="operation not supported", - gas_wanted=0, - gas_used=0, - events=Events([]), - codespace="", - ) - return cast(AbciMessage, reply) - - def deliver_tx( # pylint: disable=no-self-use - self, message: AbciMessage, dialogue: AbciDialogue - ) -> AbciMessage: - """ - Handle a message of REQUEST_DELIVER_TX performative. - - :param message: the ABCI request. - :param dialogue: the ABCI dialogue. - :return: the response. - """ - reply = dialogue.reply( - performative=AbciMessage.Performative.RESPONSE_DELIVER_TX, - target_message=message, - code=ERROR_CODE, - data=b"", - log="operation not supported", - info="operation not supported", - gas_wanted=0, - gas_used=0, - events=Events([]), - codespace="", - ) - return cast(AbciMessage, reply) - - def begin_block( # pylint: disable=no-self-use - self, message: AbciMessage, dialogue: AbciDialogue - ) -> AbciMessage: - """ - Handle a message of REQUEST_BEGIN_BLOCK performative. - - :param message: the ABCI request. - :param dialogue: the ABCI dialogue. - :return: the response. - """ - reply = dialogue.reply( - performative=AbciMessage.Performative.RESPONSE_BEGIN_BLOCK, - target_message=message, - events=Events([]), - ) - return cast(AbciMessage, reply) - - def end_block( # pylint: disable=no-self-use - self, message: AbciMessage, dialogue: AbciDialogue - ) -> AbciMessage: - """ - Handle a message of REQUEST_END_BLOCK performative. - - :param message: the ABCI request. - :param dialogue: the ABCI dialogue. - :return: the response. - """ - reply = dialogue.reply( - performative=AbciMessage.Performative.RESPONSE_END_BLOCK, - target_message=message, - validator_updates=ValidatorUpdates([]), - events=Events([]), - ) - return cast(AbciMessage, reply) - - def commit( # pylint: disable=no-self-use - self, message: AbciMessage, dialogue: AbciDialogue - ) -> AbciMessage: - """ - Handle a message of REQUEST_COMMIT performative. - - :param message: the ABCI request. - :param dialogue: the ABCI dialogue. - :return: the response. - """ - reply = dialogue.reply( - performative=AbciMessage.Performative.RESPONSE_COMMIT, - target_message=message, - data=b"", - retain_height=0, - ) - return cast(AbciMessage, reply) - - def list_snapshots( # pylint: disable=no-self-use - self, - message: AbciMessage, - dialogue: AbciDialogue, - ) -> AbciMessage: - """ - Handle a message of REQUEST_LIST_SNAPSHOT performative. - - :param message: the ABCI request. - :param dialogue: the ABCI dialogue. - :return: the response. - """ - reply = dialogue.reply( - performative=AbciMessage.Performative.RESPONSE_LIST_SNAPSHOTS, - target_message=message, - snapshots=SnapShots([]), - ) - return cast(AbciMessage, reply) - - def offer_snapshot( # pylint: disable=no-self-use - self, - message: AbciMessage, - dialogue: AbciDialogue, - ) -> AbciMessage: - """ - Handle a message of REQUEST_OFFER_SNAPSHOT performative. - - :param message: the ABCI request. - :param dialogue: the ABCI dialogue. - :return: the response. - """ - reply = dialogue.reply( - performative=AbciMessage.Performative.RESPONSE_OFFER_SNAPSHOT, - target_message=message, - result=Result(ResultType.REJECT), # by default, we reject - ) - return cast(AbciMessage, reply) - - def load_snapshot_chunk( # pylint: disable=no-self-use - self, - message: AbciMessage, - dialogue: AbciDialogue, - ) -> AbciMessage: - """ - Handle a message of REQUEST_LOAD_SNAPSHOT_CHUNK performative. - - :param message: the ABCI request. - :param dialogue: the ABCI dialogue. - :return: the response. - """ - reply = dialogue.reply( - performative=AbciMessage.Performative.RESPONSE_LOAD_SNAPSHOT_CHUNK, - target_message=message, - chunk=b"", - ) - return cast(AbciMessage, reply) - - def apply_snapshot_chunk( # pylint: disable=no-self-use - self, - message: AbciMessage, - dialogue: AbciDialogue, - ) -> AbciMessage: - """ - Handle a message of REQUEST_APPLY_SNAPSHOT_CHUNK performative. - - :param message: the ABCI request. - :param dialogue: the ABCI dialogue. - :return: the response. - """ - reply = dialogue.reply( - performative=AbciMessage.Performative.RESPONSE_APPLY_SNAPSHOT_CHUNK, - target_message=message, - result=Result(ResultType.REJECT), - refetch_chunks=tuple(), - reject_senders=tuple(), - ) - return cast(AbciMessage, reply) diff --git a/packages/valory/skills/abstract_abci/skill.yaml b/packages/valory/skills/abstract_abci/skill.yaml deleted file mode 100644 index 6811750..0000000 --- a/packages/valory/skills/abstract_abci/skill.yaml +++ /dev/null @@ -1,34 +0,0 @@ -name: abstract_abci -author: valory -version: 0.1.0 -type: skill -description: The abci skill provides a template of an ABCI application. -license: Apache-2.0 -aea_version: '>=1.0.0, <2.0.0' -fingerprint: - README.md: bafybeiezmhsokdhxat2gzxgau2zotd5nqjepg5lb2y7ypijuuq75xnxxrq - __init__.py: bafybeigdpqcsxpxp3akxdy5wcccfahom7pmbrnmututws2fmpcr7q6ryoe - dialogues.py: bafybeib6cex55nl57xe6boa4c3z4ynlxstnospqjehdb5owpgtvzsu5ucm - handlers.py: bafybeihb25swvt26vtqpnvldf6viizt34ophj6hijfpu5pevrlmvpvzkdq - tests/__init__.py: bafybeicnx4gezk2zrgz23mco2kv7ws3yd5yspku5e3ng4cb5tw7s2zexsu - tests/test_dialogues.py: bafybeig3kubiyq7bqmetrka67fjk7vymgtjwguyui3yubbvgtzzhfizsdu - tests/test_handlers.py: bafybeieeuwtu35ddaevr2wgnk33l7kdhrx7ruoeb5jiltiyn65ufdcnopu -fingerprint_ignore_patterns: [] -connections: -- valory/abci:0.1.0:bafybeie4eixvrdpc5ifoovj24a6res6g2e22dl6di6gzib7d3fczshzyti -contracts: [] -protocols: -- valory/abci:0.1.0:bafybeiaqmp7kocbfdboksayeqhkbrynvlfzsx4uy4x6nohywnmaig4an7u -skills: [] -behaviours: {} -handlers: - abci: - args: {} - class_name: ABCIHandler -models: - abci_dialogues: - args: {} - class_name: AbciDialogues -dependencies: {} -is_abstract: true -customs: [] diff --git a/packages/valory/skills/abstract_abci/tests/__init__.py b/packages/valory/skills/abstract_abci/tests/__init__.py deleted file mode 100644 index 499994e..0000000 --- a/packages/valory/skills/abstract_abci/tests/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Tests for valory/abstract_abci skill.""" diff --git a/packages/valory/skills/abstract_abci/tests/test_dialogues.py b/packages/valory/skills/abstract_abci/tests/test_dialogues.py deleted file mode 100644 index 4de0596..0000000 --- a/packages/valory/skills/abstract_abci/tests/test_dialogues.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test the dialogues.py module of the skill.""" -from enum import Enum -from typing import Type, cast -from unittest.mock import MagicMock - -import pytest -from aea.protocols.dialogue.base import Dialogues - -from packages.valory.skills.abstract_abci.dialogues import AbciDialogue, AbciDialogues - - -@pytest.mark.parametrize( - "dialogues_cls,expected_role_from_first_message", - [ - (AbciDialogues, AbciDialogue.Role.CLIENT), - ], -) -def test_dialogues_creation( - dialogues_cls: Type[AbciDialogues], expected_role_from_first_message: Enum -) -> None: - """Test XDialogues creations.""" - dialogues = cast(Dialogues, dialogues_cls(name="", skill_context=MagicMock())) - assert ( - expected_role_from_first_message - == dialogues._role_from_first_message( # pylint: disable=protected-access - MagicMock(), MagicMock() - ) - ) diff --git a/packages/valory/skills/abstract_abci/tests/test_handlers.py b/packages/valory/skills/abstract_abci/tests/test_handlers.py deleted file mode 100644 index c653c34..0000000 --- a/packages/valory/skills/abstract_abci/tests/test_handlers.py +++ /dev/null @@ -1,398 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test the handlers.py module of the skill.""" -import logging -from pathlib import Path -from typing import Any, cast -from unittest.mock import MagicMock, patch - -from aea.configurations.data_types import PublicId -from aea.protocols.base import Address, Message -from aea.protocols.dialogue.base import Dialogue as BaseDialogue -from aea.test_tools.test_skill import BaseSkillTestCase - -from packages.valory.connections.abci.connection import PUBLIC_ID -from packages.valory.protocols.abci import AbciMessage -from packages.valory.protocols.abci.custom_types import ( - CheckTxType, - CheckTxTypeEnum, - Evidences, - Header, - LastCommitInfo, - PublicKey, - Result, - ResultType, - SnapShots, - Snapshot, - Timestamp, - ValidatorUpdate, - ValidatorUpdates, -) -from packages.valory.protocols.abci.dialogues import AbciDialogues as BaseAbciDialogues -from packages.valory.skills.abstract_abci.dialogues import AbciDialogue, AbciDialogues -from packages.valory.skills.abstract_abci.handlers import ABCIHandler, ERROR_CODE - - -PACKAGE_DIR = Path(__file__).parent.parent - - -class AbciDialoguesServer(BaseAbciDialogues): - """The dialogues class keeps track of all ABCI dialogues.""" - - def __init__(self, address: str) -> None: - """Initialize dialogues.""" - self.address = address - - def role_from_first_message( # pylint: disable=unused-argument - message: Message, receiver_address: Address - ) -> BaseDialogue.Role: - """Infer the role of the agent from an incoming/outgoing first message - - :param message: an incoming/outgoing first message - :param receiver_address: the address of the receiving agent - :return: The role of the agent - """ - return AbciDialogue.Role.SERVER - - BaseAbciDialogues.__init__( - self, - self_address=self.address, - role_from_first_message=role_from_first_message, - dialogue_class=AbciDialogue, - ) - - -class TestABCIHandlerOld(BaseSkillTestCase): - """Test ABCIHandler methods.""" - - path_to_skill = PACKAGE_DIR - abci_handler: ABCIHandler - logger: logging.Logger - abci_dialogues: AbciDialogues - - @classmethod - def setup_class(cls, **kwargs: Any) -> None: - """Setup the test class.""" - super().setup_class() - cls.abci_handler = cast(ABCIHandler, cls._skill.skill_context.handlers.abci) - cls.logger = cls._skill.skill_context.logger - - cls.abci_dialogues = cast( - AbciDialogues, cls._skill.skill_context.abci_dialogues - ) - - def test_setup(self) -> None: - """Test the setup method of the echo handler.""" - with patch.object(self.logger, "log") as mock_logger: - self.abci_handler.setup() - - # after - self.assert_quantity_in_outbox(0) - - mock_logger.assert_any_call( - logging.DEBUG, f"ABCI Handler: setup method called. Using {PUBLIC_ID}." - ) - - def test_teardown(self) -> None: - """Test the teardown method of the echo handler.""" - with patch.object(self.logger, "log") as mock_logger: - self.abci_handler.teardown() - - # after - self.assert_quantity_in_outbox(0) - - mock_logger.assert_any_call( - logging.DEBUG, "ABCI Handler: teardown method called." - ) - - -class TestABCIHandler: - """Test 'ABCIHandler'.""" - - def setup(self) -> None: - """Set up the tests.""" - self.skill_id = ( # pylint: disable=attribute-defined-outside-init - PublicId.from_str("dummy/skill:0.1.0") - ) - self.context = MagicMock( # pylint: disable=attribute-defined-outside-init - skill_id=self.skill_id - ) - self.context.abci_dialogues = AbciDialogues(name="", skill_context=self.context) - self.dialogues = ( # pylint: disable=attribute-defined-outside-init - AbciDialoguesServer(address="server") - ) - self.handler = ABCIHandler( # pylint: disable=attribute-defined-outside-init - name="", skill_context=self.context - ) - - def test_setup(self) -> None: - """Test the setup method.""" - self.handler.setup() - - def test_teardown(self) -> None: - """Test the teardown method.""" - self.handler.teardown() - - def test_handle(self) -> None: - """Test the message gets handled.""" - message, _ = self.dialogues.create( - counterparty=str(self.skill_id), - performative=AbciMessage.Performative.REQUEST_INFO, - version="", - block_version=0, - p2p_version=0, - ) - self.handler.handle(cast(AbciMessage, message)) - - def test_handle_log_exception(self) -> None: - """Test the message gets handled.""" - message = AbciMessage( - dialogue_reference=("", ""), - performative=AbciMessage.Performative.REQUEST_INFO, # type: ignore - version="", - block_version=0, - p2p_version=0, - target=0, - message_id=1, - ) - message._sender = "server" # pylint: disable=protected-access - message._to = str(self.skill_id) # pylint: disable=protected-access - self.handler.handle(message) - - def test_info(self) -> None: - """Test the 'info' handler method.""" - message, dialogue = self.dialogues.create( - counterparty="", - performative=AbciMessage.Performative.REQUEST_INFO, - version="", - block_version=0, - p2p_version=0, - ) - response = self.handler.info( - cast(AbciMessage, message), cast(AbciDialogue, dialogue) - ) - assert response.performative == AbciMessage.Performative.RESPONSE_INFO - - def test_echo(self) -> None: - """Test the 'echo' handler method.""" - expected_message = "message" - message, dialogue = self.dialogues.create( - counterparty="", - performative=AbciMessage.Performative.REQUEST_ECHO, - message=expected_message, - ) - response = self.handler.echo( - cast(AbciMessage, message), cast(AbciDialogue, dialogue) - ) - assert response.performative == AbciMessage.Performative.RESPONSE_ECHO - assert response.message == expected_message - - def test_set_option(self) -> None: - """Test the 'set_option' handler method.""" - message, dialogue = self.dialogues.create( - counterparty="", - performative=AbciMessage.Performative.REQUEST_SET_OPTION, - ) - response = self.handler.set_option( - cast(AbciMessage, message), cast(AbciDialogue, dialogue) - ) - assert response.performative == AbciMessage.Performative.RESPONSE_SET_OPTION - assert response.code == ERROR_CODE - - def test_begin_block(self) -> None: - """Test the 'begin_block' handler method.""" - header = Header(*(MagicMock() for _ in range(14))) - last_commit_info = LastCommitInfo(*(MagicMock() for _ in range(2))) - byzantine_validators = Evidences(MagicMock()) - message, dialogue = self.dialogues.create( - counterparty="", - performative=AbciMessage.Performative.REQUEST_BEGIN_BLOCK, - hash=b"", - header=header, - last_commit_info=last_commit_info, - byzantine_validators=byzantine_validators, - ) - response = self.handler.begin_block( - cast(AbciMessage, message), cast(AbciDialogue, dialogue) - ) - assert response.performative == AbciMessage.Performative.RESPONSE_BEGIN_BLOCK - - def test_check_tx(self, *_: Any) -> None: - """Test the 'check_tx' handler method.""" - message, dialogue = self.dialogues.create( - counterparty="", - performative=AbciMessage.Performative.REQUEST_CHECK_TX, - tx=b"", - type=CheckTxType(CheckTxTypeEnum.NEW), - ) - response = self.handler.check_tx( - cast(AbciMessage, message), cast(AbciDialogue, dialogue) - ) - assert response.performative == AbciMessage.Performative.RESPONSE_CHECK_TX - assert response.code == ERROR_CODE - - def test_deliver_tx(self, *_: Any) -> None: - """Test the 'deliver_tx' handler method.""" - message, dialogue = self.dialogues.create( - counterparty="", - performative=AbciMessage.Performative.REQUEST_DELIVER_TX, - tx=b"", - ) - response = self.handler.deliver_tx( - cast(AbciMessage, message), cast(AbciDialogue, dialogue) - ) - assert response.performative == AbciMessage.Performative.RESPONSE_DELIVER_TX - assert response.code == ERROR_CODE - - def test_end_block(self) -> None: - """Test the 'end_block' handler method.""" - message, dialogue = self.dialogues.create( - counterparty="", - performative=AbciMessage.Performative.REQUEST_END_BLOCK, - height=1, - ) - response = self.handler.end_block( - cast(AbciMessage, message), cast(AbciDialogue, dialogue) - ) - assert response.performative == AbciMessage.Performative.RESPONSE_END_BLOCK - - def test_commit(self) -> None: - """Test the 'commit' handler method.""" - message, dialogue = self.dialogues.create( - counterparty="", - performative=AbciMessage.Performative.REQUEST_COMMIT, - ) - response = self.handler.commit( - cast(AbciMessage, message), cast(AbciDialogue, dialogue) - ) - assert response.performative == AbciMessage.Performative.RESPONSE_COMMIT - - def test_flush(self) -> None: - """Test the 'flush' handler method.""" - message, dialogue = self.dialogues.create( - counterparty="", - performative=AbciMessage.Performative.REQUEST_FLUSH, - ) - response = self.handler.flush( - cast(AbciMessage, message), cast(AbciDialogue, dialogue) - ) - assert response.performative == AbciMessage.Performative.RESPONSE_FLUSH - - def test_init_chain(self) -> None: - """Test the 'init_chain' handler method.""" - message, dialogue = self.dialogues.create( - counterparty="", - performative=AbciMessage.Performative.REQUEST_INIT_CHAIN, - time=Timestamp(1, 1), - chain_id="", - validators=ValidatorUpdates( - [ - ValidatorUpdate( - PublicKey(data=b"", key_type=PublicKey.PublicKeyType.ed25519), 1 - ) - ] - ), - app_state_bytes=b"", - initial_height=0, - ) - response = self.handler.init_chain( - cast(AbciMessage, message), cast(AbciDialogue, dialogue) - ) - assert response.performative == AbciMessage.Performative.RESPONSE_INIT_CHAIN - - def test_query(self) -> None: - """Test the 'init_chain' handler method.""" - message, dialogue = self.dialogues.create( - counterparty="", - performative=AbciMessage.Performative.REQUEST_QUERY, - query_data=b"", - path="", - height=0, - prove=True, - ) - response = self.handler.query( - cast(AbciMessage, message), cast(AbciDialogue, dialogue) - ) - assert response.performative == AbciMessage.Performative.RESPONSE_QUERY - assert response.code == ERROR_CODE - - def test_list_snapshots(self) -> None: - """Test the 'list_snapshots' handler method.""" - message, dialogue = self.dialogues.create( - counterparty="", - performative=AbciMessage.Performative.REQUEST_LIST_SNAPSHOTS, - ) - response = self.handler.list_snapshots( - cast(AbciMessage, message), cast(AbciDialogue, dialogue) - ) - assert response.performative == AbciMessage.Performative.RESPONSE_LIST_SNAPSHOTS - assert response.snapshots == SnapShots([]) - - def test_offer_snapshot(self) -> None: - """Test the 'offer_snapshot' handler method.""" - message, dialogue = self.dialogues.create( - counterparty="", - performative=AbciMessage.Performative.REQUEST_OFFER_SNAPSHOT, - snapshot=Snapshot(0, 0, 0, b"", b""), - app_hash=b"", - ) - response = self.handler.offer_snapshot( - cast(AbciMessage, message), cast(AbciDialogue, dialogue) - ) - assert response.performative == AbciMessage.Performative.RESPONSE_OFFER_SNAPSHOT - assert response.result == Result(ResultType.REJECT) - - def test_load_snapshot_chunk(self) -> None: - """Test the 'load_snapshot_chunk' handler method.""" - message, dialogue = self.dialogues.create( - counterparty="", - performative=AbciMessage.Performative.REQUEST_LOAD_SNAPSHOT_CHUNK, - height=0, - format=0, - chunk_index=0, - ) - response = self.handler.load_snapshot_chunk( - cast(AbciMessage, message), cast(AbciDialogue, dialogue) - ) - assert ( - response.performative - == AbciMessage.Performative.RESPONSE_LOAD_SNAPSHOT_CHUNK - ) - assert response.chunk == b"" - - def test_apply_snapshot_chunk(self) -> None: - """Test the 'apply_snapshot_chunk' handler method.""" - message, dialogue = self.dialogues.create( - counterparty="", - performative=AbciMessage.Performative.REQUEST_APPLY_SNAPSHOT_CHUNK, - index=0, - chunk=b"", - chunk_sender="", - ) - response = self.handler.apply_snapshot_chunk( - cast(AbciMessage, message), cast(AbciDialogue, dialogue) - ) - assert ( - response.performative - == AbciMessage.Performative.RESPONSE_APPLY_SNAPSHOT_CHUNK - ) - assert response.result == Result(ResultType.REJECT) - assert response.refetch_chunks == tuple() - assert response.reject_senders == tuple() diff --git a/packages/valory/skills/abstract_round_abci/README.md b/packages/valory/skills/abstract_round_abci/README.md deleted file mode 100644 index 4ee033e..0000000 --- a/packages/valory/skills/abstract_round_abci/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# Abstract round abci - -## Description - -This module contains an abstract round ABCI skill template for an AEA. - -## Behaviours - -* `AbstractRoundBehaviour` - - This behaviour implements an abstract round behaviour. - -* `_MetaRoundBehaviour` - - A metaclass that validates AbstractRoundBehaviour's attributes. - - -## Handlers - -* `ABCIRoundHandler` - - ABCI handler. - -* `AbstractResponseHandler` - - The concrete classes must set the `allowed_response_performatives` - class attribute to the (frozen)set of performative the developer - wants the handler to handle. - -* `ContractApiHandler` - - Implement the contract api handler. - -* `HttpHandler` - - The HTTP response handler. - -* `LedgerApiHandler` - - Implement the ledger handler. - -* `SigningHandler` - - Implement the transaction handler. - - diff --git a/packages/valory/skills/abstract_round_abci/__init__.py b/packages/valory/skills/abstract_round_abci/__init__.py deleted file mode 100644 index 2bc8fd5..0000000 --- a/packages/valory/skills/abstract_round_abci/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains an abstract round ABCI skill template for an AEA.""" # pragma: nocover - -from aea.configurations.base import PublicId # pragma: nocover - - -PUBLIC_ID = PublicId.from_str("valory/abstract_round_abci:0.1.0") diff --git a/packages/valory/skills/abstract_round_abci/abci_app_chain.py b/packages/valory/skills/abstract_round_abci/abci_app_chain.py deleted file mode 100644 index 577fb8c..0000000 --- a/packages/valory/skills/abstract_round_abci/abci_app_chain.py +++ /dev/null @@ -1,291 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains utilities for AbciApps.""" -import logging -from copy import deepcopy -from typing import Any, Dict, FrozenSet, List, Optional, Set, Tuple, Type - -from aea.exceptions import enforce - -from packages.valory.skills.abstract_round_abci.base import ( - AbciApp, - AbciAppTransitionFunction, - AppState, - EventToTimeout, - EventType, -) - - -_default_logger = logging.getLogger( - "aea.packages.valory.skills.abstract_round_abci.abci_app_chain" -) - -AbciAppTransitionMapping = Dict[AppState, AppState] - - -def check_set_uniqueness(sets: Tuple) -> Optional[Any]: - """Checks that all elements in the set list are unique and not repeated among different sets""" - all_elements = set.union(*sets) - for element in all_elements: - # Count the number of sets that include this element - sets_in = [set_ for set_ in sets if element in set_] - if len(sets_in) > 1: - return element - return None - - -def chain( # pylint: disable=too-many-locals,too-many-statements - abci_apps: Tuple[Type[AbciApp], ...], - abci_app_transition_mapping: AbciAppTransitionMapping, -) -> Type[AbciApp]: - """ - Concatenate multiple AbciApp types. - - The consistency checks assume that the first element in - abci_apps is the entry-point abci_app (i.e. the associated round of - the initial_behaviour_cls of the AbstractRoundBehaviour in which - the chained AbciApp is used is one of the initial_states of the first element.) - """ - enforce( - len(abci_apps) > 1, - f"there must be a minimum of two AbciApps to chain, found ({len(abci_apps)})", - ) - enforce( - len(set(abci_apps)) == len(abci_apps), - "Found multiple occurrences of same Abci App", - ) - non_abstract_abci_apps = [ - abci_app.__name__ for abci_app in abci_apps if not abci_app.is_abstract() - ] - enforce( - len(non_abstract_abci_apps) == 0, - f"found non-abstract AbciApp during chaining: {non_abstract_abci_apps}", - ) - - # Get the apps rounds - rounds = tuple(app.get_all_rounds() for app in abci_apps) - round_ids = tuple( - {round_.auto_round_id() for round_ in app.get_all_rounds()} for app in abci_apps - ) - - # Ensure there are no common rounds - common_round_classes = check_set_uniqueness(rounds) - enforce( - not common_round_classes, - f"rounds in common between abci apps are not allowed ({common_round_classes})", - ) - - # Ensure there are no common round_ids - common_round_ids = check_set_uniqueness(round_ids) - enforce( - not common_round_ids, - f"round ids in common between abci apps are not allowed ({common_round_ids})", - ) - - # Ensure all states in app transition mapping (keys and values) are final states or initial states, respectively. - all_final_states = { - final_state for app in abci_apps for final_state in app.final_states - } - all_initial_states = { - initial_state for app in abci_apps for initial_state in app.initial_states - }.union({app.initial_round_cls for app in abci_apps}) - for key, value in abci_app_transition_mapping.items(): - if key not in all_final_states: - raise ValueError( - f"Found non-final state {key} specified in abci_app_transition_mapping." - ) - if value not in all_initial_states: - raise ValueError( - f"Found non-initial state {value} specified in abci_app_transition_mapping." - ) - - # Ensure all DB pre- and post-conditions are consistent - # Since we know which app is the "entry-point" we can - # simply work forward from there through all branches. When - # we loop back on an earlier node we stop. - initial_state_to_app: Dict[AppState, Type[AbciApp]] = {} - for value in abci_app_transition_mapping.values(): - for app in abci_apps: - if value in app.initial_states or value == app.initial_round_cls: - initial_state_to_app[value] = app - break - - def get_paths( - initial_state: AppState, - app: Type[AbciApp], - previous_apps: Optional[List[Type[AbciApp]]] = None, - ) -> List[List[Tuple[AppState, Type[AbciApp], Optional[AppState]]]]: - """Get paths.""" - previous_apps_: List[Type[AbciApp]] = ( - deepcopy(previous_apps) if previous_apps is not None else [] - ) - default: List[List[Tuple[AppState, Type[AbciApp], Optional[AppState]]]] = [ - [(initial_state, app, None)] - ] - if app.final_states == {}: - return default # pragma: no cover - paths: List[List[Tuple[AppState, Type[AbciApp], Optional[AppState]]]] = [] - for final_state in app.final_states: - element: Tuple[AppState, Type[AbciApp], Optional[AppState]] = ( - initial_state, - app, - final_state, - ) - if final_state not in abci_app_transition_mapping: - # no linkage defined - paths.append([element]) - continue - next_initial_state = abci_app_transition_mapping[final_state] - next_app = initial_state_to_app[next_initial_state] - if next_app in previous_apps_: - # self-loops do not require attention - # we don't append to path - continue - new_previous_apps = previous_apps_ + [app] - for path in get_paths(next_initial_state, next_app, new_previous_apps): - # if element not in path: - paths.append([element] + path) - return paths if paths else default - - all_paths: List[ - List[Tuple[AppState, Type[AbciApp], Optional[AppState]]] - ] = get_paths(abci_apps[0].initial_round_cls, abci_apps[0]) - new_db_post_conditions: Dict[AppState, Set[str]] = {} - for path in all_paths: - current_initial_state, current_app, current_final_state = path[0] - accumulated_post_conditions: Set[str] = current_app.db_pre_conditions.get( - current_initial_state, set() - ) - for next_initial_state, next_app, next_final_state in path[1:]: - if current_final_state is None: - # No outwards transition, nothing to check. - # we are at the end of a path where the last - # app has no final state and therefore no post conditions - break # pragma: no cover - accumulated_post_conditions = accumulated_post_conditions.union( - set(current_app.db_post_conditions[current_final_state]) - ) - # we now check that the pre conditions of the next app - # are compatible with the post conditions of the current apps. - if next_initial_state in next_app.db_pre_conditions: - diff = set.difference( - next_app.db_pre_conditions[next_initial_state], - accumulated_post_conditions, - ) - if len(diff) != 0: - raise ValueError( - f"Pre conditions '{diff}' of app '{next_app}' not a post condition of app '{current_app}' or any preceding app in path {path}." - ) - else: - raise ValueError( - f"No pre-conditions have been set for {next_initial_state}! " - f"You need to explicitly specify them as empty if there are no pre-conditions for this FSM." - ) - current_app = next_app - current_final_state = next_final_state - - if current_final_state is not None: - new_db_post_conditions[current_final_state] = accumulated_post_conditions - - # Warn about events duplicated in multiple apps - app_to_events = {app: app.get_all_events() for app in abci_apps} - all_events = set.union(*app_to_events.values()) - for event in all_events: - apps = [str(app) for app, events in app_to_events.items() if event in events] - if len(apps) > 1: - apps_str = "\n".join(apps) - _default_logger.warning( - f"The same event '{event}' has been found in several apps:\n{apps_str}\n" - "It will be interpreted as the same event. " - "If this is not the intended behaviour, please rename it to enforce its uniqueness." - ) - - new_initial_round_cls = abci_apps[0].initial_round_cls - new_initial_states = abci_apps[0].initial_states - new_db_pre_conditions = abci_apps[0].db_pre_conditions - - # Merge the transition functions, final states and events - potential_final_states = set.union(*(app.final_states for app in abci_apps)) - potential_events_to_timeout: EventToTimeout = {} - for app in abci_apps: - for e, t in app.event_to_timeout.items(): - if e in potential_events_to_timeout and potential_events_to_timeout[e] != t: - raise ValueError( - f"Event {e} defined in app {app} is defined with timeout {t} but it is already defined in a prior app with timeout {potential_events_to_timeout[e]}." - ) - potential_events_to_timeout[e] = t - - potential_transition_function: AbciAppTransitionFunction = {} - for app in abci_apps: - for state, events_to_rounds in app.transition_function.items(): - if state in abci_app_transition_mapping: - # we remove these final states - continue - # Update transition function according to the transition mapping - new_events_to_rounds = {} - for event, round_ in events_to_rounds.items(): - destination_round = abci_app_transition_mapping.get(round_, round_) - new_events_to_rounds[event] = destination_round - potential_transition_function[state] = new_events_to_rounds - - # Remove no longer used states from transition function and final states - destination_states: Set[AppState] = set() - for event_to_states in potential_transition_function.values(): - destination_states.update(event_to_states.values()) - new_transition_function: AbciAppTransitionFunction = { - state: events_to_rounds - for state, events_to_rounds in potential_transition_function.items() - if state in destination_states or state is new_initial_round_cls - } - new_final_states = { - state for state in potential_final_states if state in destination_states - } - - # Remove no longer used events - used_events: Set[str] = set() - for event_to_states in new_transition_function.values(): - used_events.update(event_to_states.keys()) - new_events_to_timeout = { - event: timeout - for event, timeout in potential_events_to_timeout.items() - if event in used_events - } - - # Collect keys to persist across periods from all abcis - new_cross_period_persisted_keys: Set[str] = set() - for app in abci_apps: - new_cross_period_persisted_keys.update(app.cross_period_persisted_keys) - - # Return the composed result - class ComposedAbciApp(AbciApp[EventType]): - """Composed abci app class.""" - - initial_round_cls: AppState = new_initial_round_cls - initial_states: Set[AppState] = new_initial_states - transition_function: AbciAppTransitionFunction = new_transition_function - final_states: Set[AppState] = new_final_states - event_to_timeout: EventToTimeout = new_events_to_timeout - cross_period_persisted_keys: FrozenSet[str] = frozenset( - new_cross_period_persisted_keys - ) - db_pre_conditions: Dict[AppState, Set[str]] = new_db_pre_conditions - db_post_conditions: Dict[AppState, Set[str]] = new_db_post_conditions - - return ComposedAbciApp diff --git a/packages/valory/skills/abstract_round_abci/base.py b/packages/valory/skills/abstract_round_abci/base.py deleted file mode 100644 index 9718c8e..0000000 --- a/packages/valory/skills/abstract_round_abci/base.py +++ /dev/null @@ -1,3850 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the base classes for the models classes of the skill.""" - -import datetime -import hashlib -import heapq -import itertools -import json -import logging -import re -import sys -import textwrap -import uuid -from abc import ABC, ABCMeta, abstractmethod -from collections import Counter, deque -from copy import copy, deepcopy -from dataclasses import asdict, astuple, dataclass, field, is_dataclass -from enum import Enum -from inspect import isclass -from math import ceil -from typing import ( - Any, - Callable, - Deque, - Dict, - FrozenSet, - Generic, - Iterator, - List, - Mapping, - Optional, - Sequence, - Set, - Tuple, - Type, - TypeVar, - Union, - cast, -) - -from aea.crypto.ledger_apis import LedgerApis -from aea.exceptions import enforce -from aea.skills.base import SkillContext - -from packages.valory.connections.abci.connection import MAX_READ_IN_BYTES -from packages.valory.connections.ledger.connection import ( - PUBLIC_ID as LEDGER_CONNECTION_PUBLIC_ID, -) -from packages.valory.protocols.abci.custom_types import ( - EvidenceType, - Evidences, - Header, - LastCommitInfo, - Validator, -) -from packages.valory.skills.abstract_round_abci.utils import ( - consensus_threshold, - is_json_serializable, -) - - -_logger = logging.getLogger("aea.packages.valory.skills.abstract_round_abci.base") - -OK_CODE = 0 -ERROR_CODE = 1 -LEDGER_API_ADDRESS = str(LEDGER_CONNECTION_PUBLIC_ID) -ROUND_COUNT_DEFAULT = -1 -MIN_HISTORY_DEPTH = 1 -ADDRESS_LENGTH = 42 -MAX_INT_256 = 2**256 - 1 -RESET_COUNT_START = 0 -VALUE_NOT_PROVIDED = object() -# tolerance in seconds for new blocks not having arrived yet -BLOCKS_STALL_TOLERANCE = 60 -SERIOUS_OFFENCE_ENUM_MIN = 1000 -NUMBER_OF_BLOCKS_TRACKED = 10_000 -NUMBER_OF_ROUNDS_TRACKED = 50 - -EventType = TypeVar("EventType") - - -def get_name(prop: Any) -> str: - """Get the name of a property.""" - if not (isinstance(prop, property) and hasattr(prop, "fget")): - raise ValueError(f"{prop} is not a property") - if prop.fget is None: - raise ValueError(f"fget of {prop} is None") # pragma: nocover - return prop.fget.__name__ - - -class ABCIAppException(Exception): - """A parent class for all exceptions related to the ABCIApp.""" - - -class SignatureNotValidError(ABCIAppException): - """Error raised when a signature is invalid.""" - - -class AddBlockError(ABCIAppException): - """Exception raised when a block addition is not valid.""" - - -class ABCIAppInternalError(ABCIAppException): - """Internal error due to a bad implementation of the ABCIApp.""" - - def __init__(self, message: str, *args: Any) -> None: - """Initialize the error object.""" - super().__init__("internal error: " + message, *args) - - -class TransactionTypeNotRecognizedError(ABCIAppException): - """Error raised when a transaction type is not recognized.""" - - -class TransactionNotValidError(ABCIAppException): - """Error raised when a transaction is not valid.""" - - -class LateArrivingTransaction(ABCIAppException): - """Error raised when the transaction belongs to previous round.""" - - -class AbstractRoundInternalError(ABCIAppException): - """Internal error due to a bad implementation of the AbstractRound.""" - - def __init__(self, message: str, *args: Any) -> None: - """Initialize the error object.""" - super().__init__("internal error: " + message, *args) - - -class _MetaPayload(ABCMeta): - """ - Payload metaclass. - - The purpose of this metaclass is to remember the association - between the type of payload and the payload class to build it. - This is necessary to recover the right payload class to instantiate - at decoding time. - """ - - registry: Dict[str, Type["BaseTxPayload"]] = {} - - def __new__(mcs, name: str, bases: Tuple, namespace: Dict, **kwargs: Any) -> Type: # type: ignore - """Create a new class object.""" - new_cls = super().__new__(mcs, name, bases, namespace, **kwargs) - - if new_cls.__module__ == mcs.__module__ and new_cls.__name__ == "BaseTxPayload": - return new_cls - if not issubclass(new_cls, BaseTxPayload): - raise ValueError( # pragma: no cover - f"class {name} must inherit from {BaseTxPayload.__name__}" - ) - new_cls = cast(Type[BaseTxPayload], new_cls) - # remember association from transaction type to payload class - _metaclass_registry_key = f"{new_cls.__module__}.{new_cls.__name__}" # type: ignore - mcs.registry[_metaclass_registry_key] = new_cls - - return new_cls - - -@dataclass(frozen=True) -class BaseTxPayload(metaclass=_MetaPayload): - """This class represents a base class for transaction payload classes.""" - - sender: str - round_count: int = field(default=ROUND_COUNT_DEFAULT, init=False) - id_: str = field(default_factory=lambda: uuid.uuid4().hex, init=False) - - @property - def data(self) -> Dict[str, Any]: - """Data""" - excluded = ["sender", "round_count", "id_"] - return {k: v for k, v in asdict(self).items() if k not in excluded} - - @property - def values(self) -> Tuple[Any, ...]: - """Data""" - excluded = 3 # refers to ["sender", "round_count", "id_"] - return astuple(self)[excluded:] - - @property - def json(self) -> Dict[str, Any]: - """Json""" - data, cls = asdict(self), self.__class__ - data["_metaclass_registry_key"] = f"{cls.__module__}.{cls.__name__}" - return data - - @classmethod - def from_json(cls, obj: Dict) -> "BaseTxPayload": - """Decode the payload.""" - data = copy(obj) - round_count, id_ = data.pop("round_count"), data.pop("id_") - payload_cls = _MetaPayload.registry[data.pop("_metaclass_registry_key")] - payload = payload_cls(**data) # type: ignore - object.__setattr__(payload, "round_count", round_count) - object.__setattr__(payload, "id_", id_) - return payload - - def with_new_id(self) -> "BaseTxPayload": - """Create a new payload with the same content but new id.""" - new = type(self)(sender=self.sender, **self.data) # type: ignore - object.__setattr__(new, "round_count", self.round_count) - return new - - def encode(self) -> bytes: - """Encode""" - encoded_data = json.dumps(self.json, sort_keys=True).encode() - if sys.getsizeof(encoded_data) > MAX_READ_IN_BYTES: - msg = f"{type(self)} must be smaller than {MAX_READ_IN_BYTES} bytes" - raise ValueError(msg) - return encoded_data - - @classmethod - def decode(cls, obj: bytes) -> "BaseTxPayload": - """Decode""" - return cls.from_json(json.loads(obj.decode())) - - -@dataclass(frozen=True) -class Transaction(ABC): - """Class to represent a transaction for the ephemeral chain of a period.""" - - payload: BaseTxPayload - signature: str - - def encode(self) -> bytes: - """Encode the transaction.""" - - data = dict(payload=self.payload.json, signature=self.signature) - encoded_data = json.dumps(data, sort_keys=True).encode() - if sys.getsizeof(encoded_data) > MAX_READ_IN_BYTES: - raise ValueError( - f"Transaction must be smaller than {MAX_READ_IN_BYTES} bytes" - ) - return encoded_data - - @classmethod - def decode(cls, obj: bytes) -> "Transaction": - """Decode the transaction.""" - - data = json.loads(obj.decode()) - signature = data["signature"] - payload = BaseTxPayload.from_json(data["payload"]) - return Transaction(payload, signature) - - def verify(self, ledger_id: str) -> None: - """ - Verify the signature is correct. - - :param ledger_id: the ledger id of the address - :raises: SignatureNotValidError: if the signature is not valid. - """ - payload_bytes = self.payload.encode() - addresses = LedgerApis.recover_message( - identifier=ledger_id, message=payload_bytes, signature=self.signature - ) - if self.payload.sender not in addresses: - raise SignatureNotValidError(f"Signature not valid on transaction: {self}") - - -class Block: # pylint: disable=too-few-public-methods - """Class to represent (a subset of) data of a Tendermint block.""" - - def __init__( - self, - header: Header, - transactions: Sequence[Transaction], - ) -> None: - """Initialize the block.""" - self.header = header - self._transactions: Tuple[Transaction, ...] = tuple(transactions) - - @property - def transactions(self) -> Tuple[Transaction, ...]: - """Get the transactions.""" - return self._transactions - - @property - def timestamp(self) -> datetime.datetime: - """Get the block timestamp.""" - return self.header.timestamp - - -class Blockchain: - """ - Class to represent a (naive) Tendermint blockchain. - - The consistency of the data in the blocks is guaranteed by Tendermint. - """ - - def __init__(self, height_offset: int = 0, is_init: bool = True) -> None: - """Initialize the blockchain.""" - self._blocks: List[Block] = [] - self._height_offset = height_offset - self._is_init = is_init - - @property - def is_init(self) -> bool: - """Returns true if the blockchain is initialized.""" - return self._is_init - - def add_block(self, block: Block) -> None: - """Add a block to the list.""" - expected_height = self.height + 1 - actual_height = block.header.height - if actual_height < self._height_offset: - # if the current block has a lower height than the - # initial height, ignore it - return - - if expected_height != actual_height: - raise AddBlockError( - f"expected height {expected_height}, got {actual_height}" - ) - self._blocks.append(block) - - @property - def height(self) -> int: - """ - Get the height. - - Tendermint's height starts from 1. A return value - equal to 0 means empty blockchain. - - :return: the height. - """ - return self.length + self._height_offset - - @property - def length(self) -> int: - """Get the blockchain length.""" - return len(self._blocks) - - @property - def blocks(self) -> Tuple[Block, ...]: - """Get the blocks.""" - return tuple(self._blocks) - - @property - def last_block( - self, - ) -> Block: - """Returns the last stored block.""" - return self._blocks[-1] - - -class BlockBuilder: - """Helper class to build a block.""" - - _current_header: Optional[Header] = None - _current_transactions: List[Transaction] = [] - - def __init__(self) -> None: - """Initialize the block builder.""" - self.reset() - - def reset(self) -> None: - """Reset the temporary data structures.""" - self._current_header = None - self._current_transactions = [] - - @property - def header(self) -> Header: - """ - Get the block header. - - :return: the block header - """ - if self._current_header is None: - raise ValueError("header not set") - return self._current_header - - @header.setter - def header(self, header: Header) -> None: - """Set the header.""" - if self._current_header is not None: - raise ValueError("header already set") - self._current_header = header - - @property - def transactions(self) -> Tuple[Transaction, ...]: - """Get the sequence of transactions.""" - return tuple(self._current_transactions) - - def add_transaction(self, transaction: Transaction) -> None: - """Add a transaction.""" - self._current_transactions.append(transaction) - - def get_block(self) -> Block: - """Get the block.""" - return Block( - self.header, - self._current_transactions, - ) - - -class AbciAppDB: - """Class to represent all data replicated across agents. - - This class stores all the data in self._data. Every entry on this dict represents an optional "period" within your app execution. - The concept of period is user-defined, so it might be something like a sequence of rounds that together conform a logical cycle of - its execution, or it might have no sense at all (thus its optionality) and therefore only period 0 will be used. - - Every "period" entry stores a dict where every key is a saved parameter and its corresponding value a list containing the history - of the parameter values. For instance, for period 0: - - 0: {"parameter_name": [parameter_history]} - - A complete database could look like this: - - data = { - 0: { - "participants": - [ - {"participant_a", "participant_b", "participant_c", "participant_d"}, - {"participant_a", "participant_b", "participant_c"}, - {"participant_a", "participant_b", "participant_c", "participant_d"}, - ] - }, - "other_parameter": [0, 2, 8] - }, - 1: { - "participants": - [ - {"participant_a", "participant_c", "participant_d"}, - {"participant_a", "participant_b", "participant_c", "participant_d"}, - {"participant_a", "participant_b", "participant_c"}, - {"participant_a", "participant_b", "participant_d"}, - {"participant_a", "participant_b", "participant_c", "participant_d"}, - ], - "other_parameter": [3, 19, 10, 32, 6] - }, - 2: ... - } - - # Adding and removing data from the current period - -------------------------------------------------- - To update the current period entry, just call update() on the class. The new values will be appended to the current list for each updated parameter. - - To clean up old data from the current period entry, call cleanup_current_histories(cleanup_history_depth_current), where cleanup_history_depth_current - is the amount of data that you want to keep after the cleanup. The newest cleanup_history_depth_current values will be kept for each parameter in the DB. - - # Creating and removing old periods - ----------------------------------- - To create a new period entry, call create() on the class. The new values will be stored in a new list for each updated parameter. - - To remove old periods, call cleanup(cleanup_history_depth, [cleanup_history_depth_current]), where cleanup_history_depth is the amount of periods - that you want to keep after the cleanup. The newest cleanup_history_depth periods will be kept. If you also specify cleanup_history_depth_current, - cleanup_current_histories will be also called (see previous point). - - The parameters cleanup_history_depth and cleanup_history_depth_current can also be configured in skill.yaml so they are used automatically - when the cleanup method is called from AbciApp.cleanup(). - - # Memory warning - ----------------------------------- - The database is implemented in such a way to avoid indirect modification of its contents. - It copies all the mutable data structures*, which means that it consumes more memory than expected. - This is necessary because otherwise it would risk chance of modification from the behaviour side, - which is a safety concern. - - The effect of this on the memory usage should not be a big concern, because: - - 1. The synchronized data of the agents are not intended to store large amount of data. - IPFS should be used in such cases, and only the hash should be synchronized in the db. - 2. The data are automatically wiped after a predefined `cleanup_history` depth as described above. - 3. The retrieved data are only meant to be used for a short amount of time, - e.g., to perform a decision on a behaviour, which means that the gc will collect them before they are noticed. - - * the in-built `copy` module is used, which automatically detects if an item is immutable and skips copying it. - For more information take a look at the `_deepcopy_atomic` method and its usage: - https://github.com/python/cpython/blob/3.10/Lib/copy.py#L182-L183 - """ - - DB_DATA_KEY = "db_data" - SLASHING_CONFIG_KEY = "slashing_config" - - # database keys which values are always set for the next period by default - default_cross_period_keys: FrozenSet[str] = frozenset( - { - "all_participants", - "participants", - "consensus_threshold", - "safe_contract_address", - } - ) - - def __init__( - self, - setup_data: Dict[str, List[Any]], - cross_period_persisted_keys: Optional[FrozenSet[str]] = None, - logger: Optional[logging.Logger] = None, - ) -> None: - """Initialize the AbciApp database. - - setup_data must be passed as a Dict[str, List[Any]] (the database internal format). - The staticmethod 'data_to_lists' can be used to convert from Dict[str, Any] to Dict[str, List[Any]] - before instantiating this class. - - :param setup_data: the setup data - :param cross_period_persisted_keys: data keys that will be kept after a new period starts - :param logger: the logger of the abci app - """ - self.logger = logger or _logger - AbciAppDB._check_data(setup_data) - self._setup_data = deepcopy(setup_data) - self._data: Dict[int, Dict[str, List[Any]]] = { - RESET_COUNT_START: self.setup_data # the key represents the reset index - } - self._round_count = ROUND_COUNT_DEFAULT # ensures first round is indexed at 0! - - self._cross_period_persisted_keys = self.default_cross_period_keys.union( - cross_period_persisted_keys or frozenset() - ) - self._cross_period_check() - self.slashing_config: str = "" - - def _cross_period_check(self) -> None: - """Check the cross period keys against the setup data.""" - not_in_cross_period = set(self._setup_data).difference( - self.cross_period_persisted_keys - ) - if not_in_cross_period: - self.logger.warning( - f"The setup data ({self._setup_data.keys()}) contain keys that are not in the " - f"cross period persisted keys ({self.cross_period_persisted_keys}): {not_in_cross_period}" - ) - - @staticmethod - def normalize(value: Any) -> str: - """Attempt to normalize a non-primitive type to insert it into the db.""" - if is_json_serializable(value): - return value - - if isinstance(value, Enum): - return value.value - - if isinstance(value, bytes): - return value.hex() - - if isinstance(value, set): - try: - return json.dumps(list(value)) - except TypeError: - pass - - raise ValueError(f"Cannot normalize {value} to insert it in the db!") - - @property - def setup_data(self) -> Dict[str, Any]: - """ - Get the setup_data without entries which have empty values. - - :return: the setup_data - """ - # do not return data if no value has been set - return {k: v for k, v in deepcopy(self._setup_data).items() if len(v)} - - @staticmethod - def _check_data(data: Any) -> None: - """Check that all fields in setup data were passed as a list, and that the data can be accepted into the db.""" - if ( - not isinstance(data, dict) - or not all((isinstance(k, str) for k in data.keys())) - or not all((isinstance(v, list) for v in data.values())) - ): - raise ValueError( - f"AbciAppDB data must be `Dict[str, List[Any]]`, found `{type(data)}` instead." - ) - - AbciAppDB.validate(data) - - @property - def reset_index(self) -> int: - """Get the current reset index.""" - # should return the last key or 0 if we have no data - return list(self._data)[-1] if self._data else 0 - - @property - def round_count(self) -> int: - """Get the round count.""" - return self._round_count - - @round_count.setter - def round_count(self, round_count: int) -> None: - """Set the round count.""" - self._round_count = round_count - - @property - def cross_period_persisted_keys(self) -> FrozenSet[str]: - """Keys in the database which are persistent across periods.""" - return self._cross_period_persisted_keys - - def get(self, key: str, default: Any = VALUE_NOT_PROVIDED) -> Optional[Any]: - """Given a key, get its last for the current reset index.""" - if key in self._data[self.reset_index]: - return deepcopy(self._data[self.reset_index][key][-1]) - if default != VALUE_NOT_PROVIDED: - return default - raise ValueError( - f"'{key}' field is not set for this period [{self.reset_index}] and no default value was provided." - ) - - def get_strict(self, key: str) -> Any: - """Get a value from the data dictionary and raise if it is None.""" - return self.get(key) - - @staticmethod - def validate(data: Any) -> None: - """Validate if the given data are json serializable and therefore can be accepted into the database. - - :param data: the data to check. - :raises ABCIAppInternalError: If the data are not serializable. - """ - if not is_json_serializable(data): - raise ABCIAppInternalError( - f"`AbciAppDB` data must be json-serializable. Please convert non-serializable data in `{data}`. " - "You may use `AbciAppDB.validate(your_data)` to validate your data for the `AbciAppDB`." - ) - - def update(self, **kwargs: Any) -> None: - """Update the current data.""" - self.validate(kwargs) - - # Append new data to the key history - data = self._data[self.reset_index] - for key, value in deepcopy(kwargs).items(): - data.setdefault(key, []).append(value) - - def create(self, **kwargs: Any) -> None: - """Add a new entry to the data. - - Passes automatically the values of the `cross_period_persisted_keys` to the next period. - - :param kwargs: keyword arguments - """ - for key in self.cross_period_persisted_keys.union(kwargs.keys()): - value = kwargs.get(key, VALUE_NOT_PROVIDED) - if value is VALUE_NOT_PROVIDED: - value = self.get_latest().get(key, VALUE_NOT_PROVIDED) - if value is VALUE_NOT_PROVIDED: - raise ABCIAppInternalError( - f"Cross period persisted key `{key}` was not found in the db but was required for the next period." - ) - if isinstance(value, (set, frozenset)): - value = tuple(sorted(value)) - kwargs[key] = value - - data = self.data_to_lists(kwargs) - self._create_from_keys(**data) - - def _create_from_keys(self, **kwargs: Any) -> None: - """Add a new entry to the data using the provided key-value pairs.""" - AbciAppDB._check_data(kwargs) - self._data[self.reset_index + 1] = deepcopy(kwargs) - - def get_latest_from_reset_index(self, reset_index: int) -> Dict[str, Any]: - """Get the latest key-value pairs from the data dictionary for the specified period.""" - return { - key: values[-1] - for key, values in deepcopy(self._data.get(reset_index, {})).items() - } - - def get_latest(self) -> Dict[str, Any]: - """Get the latest key-value pairs from the data dictionary for the current period.""" - return self.get_latest_from_reset_index(self.reset_index) - - def increment_round_count(self) -> None: - """Increment the round count.""" - self._round_count += 1 - - def __repr__(self) -> str: - """Return a string representation of the data.""" - return f"AbciAppDB({self._data})" - - def cleanup( - self, - cleanup_history_depth: int, - cleanup_history_depth_current: Optional[int] = None, - ) -> None: - """Reset the db, keeping only the latest entries (periods). - - If cleanup_history_depth_current has been also set, also clear oldest historic values in the current entry. - - :param cleanup_history_depth: depth to clean up history - :param cleanup_history_depth_current: whether or not to clean up current entry too. - """ - cleanup_history_depth = max(cleanup_history_depth, MIN_HISTORY_DEPTH) - self._data = { - key: self._data[key] - for key in sorted(self._data.keys())[-cleanup_history_depth:] - } - if cleanup_history_depth_current: - self.cleanup_current_histories(cleanup_history_depth_current) - - def cleanup_current_histories(self, cleanup_history_depth_current: int) -> None: - """Reset the parameter histories for the current entry (period), keeping only the latest values for each parameter.""" - cleanup_history_depth_current = max( - cleanup_history_depth_current, MIN_HISTORY_DEPTH - ) - self._data[self.reset_index] = { - key: history[-cleanup_history_depth_current:] - for key, history in self._data[self.reset_index].items() - } - - def serialize(self) -> str: - """Serialize the data of the database to a string.""" - db = { - self.DB_DATA_KEY: self._data, - self.SLASHING_CONFIG_KEY: self.slashing_config, - } - return json.dumps(db, sort_keys=True) - - @staticmethod - def _as_abci_data(data: Dict) -> Dict[int, Any]: - """Hook to load serialized data as `AbciAppDB` data.""" - return {int(index): content for index, content in data.items()} - - def sync(self, serialized_data: str) -> None: - """Synchronize the data using a serialized object. - - :param serialized_data: the serialized data to use in order to sync the db. - :raises ABCIAppInternalError: if the given data cannot be deserialized. - """ - try: - loaded_data = json.loads(serialized_data) - except json.JSONDecodeError as exc: - raise ABCIAppInternalError( - f"Could not decode data using {serialized_data}: {exc}" - ) from exc - - input_report = f"\nThe following serialized data were given: {serialized_data}" - try: - db_data = loaded_data[self.DB_DATA_KEY] - slashing_config = loaded_data[self.SLASHING_CONFIG_KEY] - except KeyError as exc: - raise ABCIAppInternalError( - "Mandatory keys `db_data`, `slashing_config` are missing from the deserialized data: " - f"{loaded_data}{input_report}" - ) from exc - - try: - db_data = self._as_abci_data(db_data) - except AttributeError as exc: - raise ABCIAppInternalError( - f"Could not decode db data with an invalid format: {db_data}{input_report}" - ) from exc - except ValueError as exc: - raise ABCIAppInternalError( - f"An invalid index was found while trying to sync the db using data: {db_data}{input_report}" - ) from exc - - self._check_data(dict(tuple(db_data.values())[0])) - self._data = db_data - self.slashing_config = slashing_config - - def hash(self) -> bytes: - """Create a hash of the data.""" - # Compute the sha256 hash of the serialized data - sha256 = hashlib.sha256() - data = self.serialize() - sha256.update(data.encode("utf-8")) - hash_ = sha256.digest() - self.logger.debug(f"root hash: {hash_.hex()}; data: {data}") - return hash_ - - @staticmethod - def data_to_lists(data: Dict[str, Any]) -> Dict[str, List[Any]]: - """Convert Dict[str, Any] to Dict[str, List[Any]].""" - return {k: [v] for k, v in data.items()} - - -SerializedCollection = Dict[str, Dict[str, Any]] -DeserializedCollection = Mapping[str, BaseTxPayload] - - -class BaseSynchronizedData: - """ - Class to represent the synchronized data. - - This is the relevant data constructed and replicated by the agents. - """ - - # Keys always set by default - # `round_count` and `period_count` need to be guaranteed to be synchronized too: - # - # * `round_count` is only incremented when scheduling a new round, - # which is by definition always a synchronized action. - # * `period_count` comes from the `reset_index` which is the last key of the `self._data`. - # The `self._data` keys are only updated on create, and cleanup operations, - # which are also meant to be synchronized since they are used at the rounds. - default_db_keys: Set[str] = { - "round_count", - "period_count", - "all_participants", - "nb_participants", - "max_participants", - "consensus_threshold", - "safe_contract_address", - } - - def __init__( - self, - db: AbciAppDB, - ) -> None: - """Initialize the synchronized data.""" - self._db = db - - @property - def db(self) -> AbciAppDB: - """Get DB.""" - return self._db - - @property - def round_count(self) -> int: - """Get the round count.""" - return self.db.round_count - - @property - def period_count(self) -> int: - """Get the period count. - - Periods are executions between calls to AbciAppDB.create(), so as soon as it is called, - a new period begins. It is useful to have a logical subdivision of the FSM execution. - For example, if AbciAppDB.create() is called during reset, then a period will be the - execution between resets. - - :return: the period count - """ - return self.db.reset_index - - @property - def participants(self) -> FrozenSet[str]: - """Get the currently active participants.""" - participants = frozenset(self.db.get_strict("participants")) - if len(participants) == 0: - raise ValueError("List participants cannot be empty.") - return cast(FrozenSet[str], participants) - - @property - def all_participants(self) -> FrozenSet[str]: - """Get all registered participants.""" - all_participants = frozenset(self.db.get_strict("all_participants")) - if len(all_participants) == 0: - raise ValueError("List participants cannot be empty.") - return cast(FrozenSet[str], all_participants) - - @property - def max_participants(self) -> int: - """Get the number of all the participants.""" - return len(self.all_participants) - - @property - def consensus_threshold(self) -> int: - """Get the consensus threshold.""" - threshold = self.db.get_strict("consensus_threshold") - min_threshold = consensus_threshold(self.max_participants) - - if threshold is None: - return min_threshold - - threshold = int(threshold) - max_threshold = len(self.all_participants) - - if min_threshold <= threshold <= max_threshold: - return threshold - - expected_range = ( - f"can only be {min_threshold}" - if min_threshold == max_threshold - else f"not in [{min_threshold}, {max_threshold}]" - ) - raise ValueError(f"Consensus threshold {threshold} {expected_range}.") - - @property - def sorted_participants(self) -> Sequence[str]: - """ - Get the sorted participants' addresses. - - The addresses are sorted according to their hexadecimal value; - this is the reason we use key=str.lower as comparator. - - This property is useful when interacting with the Safe contract. - - :return: the sorted participants' addresses - """ - return sorted(self.participants, key=str.lower) - - @property - def nb_participants(self) -> int: - """Get the number of participants.""" - participants = cast(List, self.db.get("participants", [])) - return len(participants) - - @property - def slashing_config(self) -> str: - """Get the slashing configuration.""" - return self.db.slashing_config - - @slashing_config.setter - def slashing_config(self, config: str) -> None: - """Set the slashing configuration.""" - self.db.slashing_config = config - - def update( - self, - synchronized_data_class: Optional[Type] = None, - **kwargs: Any, - ) -> "BaseSynchronizedData": - """Copy and update the current data.""" - self.db.update(**kwargs) - - class_ = ( - type(self) if synchronized_data_class is None else synchronized_data_class - ) - return class_(db=self.db) - - def create( - self, - synchronized_data_class: Optional[Type] = None, - ) -> "BaseSynchronizedData": - """Copy and update with new data. Set values are stored as sorted tuples to the db for determinism.""" - self.db.create() - class_ = ( - type(self) if synchronized_data_class is None else synchronized_data_class - ) - return class_(db=self.db) - - def __repr__(self) -> str: - """Return a string representation of the data.""" - return f"{self.__class__.__name__}(db={self._db})" - - @property - def keeper_randomness(self) -> float: - """Get the keeper's random number [0-1].""" - return ( - int(self.most_voted_randomness, base=16) / MAX_INT_256 - ) # DRAND uses sha256 values - - @property - def most_voted_randomness(self) -> str: - """Get the most_voted_randomness.""" - return cast(str, self.db.get_strict("most_voted_randomness")) - - @property - def most_voted_keeper_address(self) -> str: - """Get the most_voted_keeper_address.""" - return cast(str, self.db.get_strict("most_voted_keeper_address")) - - @property - def is_keeper_set(self) -> bool: - """Check whether keeper is set.""" - return self.db.get("most_voted_keeper_address", None) is not None - - @property - def blacklisted_keepers(self) -> Set[str]: - """Get the current cycle's blacklisted keepers who cannot submit a transaction.""" - raw = cast(str, self.db.get("blacklisted_keepers", "")) - return set(textwrap.wrap(raw, ADDRESS_LENGTH)) - - @property - def participant_to_selection(self) -> DeserializedCollection: - """Check whether keeper is set.""" - serialized = self.db.get_strict("participant_to_selection") - deserialized = CollectionRound.deserialize_collection(serialized) - return cast(DeserializedCollection, deserialized) - - @property - def participant_to_randomness(self) -> DeserializedCollection: - """Check whether keeper is set.""" - serialized = self.db.get_strict("participant_to_randomness") - deserialized = CollectionRound.deserialize_collection(serialized) - return cast(DeserializedCollection, deserialized) - - @property - def participant_to_votes(self) -> DeserializedCollection: - """Check whether keeper is set.""" - serialized = self.db.get_strict("participant_to_votes") - deserialized = CollectionRound.deserialize_collection(serialized) - return cast(DeserializedCollection, deserialized) - - @property - def safe_contract_address(self) -> str: - """Get the safe contract address.""" - return cast(str, self.db.get_strict("safe_contract_address")) - - -class _MetaAbstractRound(ABCMeta): - """A metaclass that validates AbstractRound's attributes.""" - - def __new__(mcs, name: str, bases: Tuple, namespace: Dict, **kwargs: Any) -> Type: # type: ignore - """Initialize the class.""" - new_cls = super().__new__(mcs, name, bases, namespace, **kwargs) - - if ABC in bases: - # abstract class, return - return new_cls - if not issubclass(new_cls, AbstractRound): - # the check only applies to AbstractRound subclasses - return new_cls - - mcs._check_consistency(cast(Type[AbstractRound], new_cls)) - return new_cls - - @classmethod - def _check_consistency(mcs, abstract_round_cls: Type["AbstractRound"]) -> None: - """Check consistency of class attributes.""" - mcs._check_required_class_attributes(abstract_round_cls) - - @classmethod - def _check_required_class_attributes( - mcs, abstract_round_cls: Type["AbstractRound"] - ) -> None: - """Check that required class attributes are set.""" - if not hasattr(abstract_round_cls, "synchronized_data_class"): - raise AbstractRoundInternalError( - f"'synchronized_data_class' not set on {abstract_round_cls}" - ) - if not hasattr(abstract_round_cls, "payload_class"): - raise AbstractRoundInternalError( - f"'payload_class' not set on {abstract_round_cls}" - ) - - -class AbstractRound(Generic[EventType], ABC, metaclass=_MetaAbstractRound): - """ - This class represents an abstract round. - - A round is a state of the FSM App execution. It usually involves - interactions between participants in the FSM App, - although this is not enforced at this level of abstraction. - - Concrete classes must set: - - synchronized_data_class: the data class associated with this round; - - payload_class: the payload type that is allowed for this round; - - Optionally, round_id can be defined, although it is recommended to use the autogenerated id. - """ - - __pattern = re.compile(r"(? None: - """Initialize the round.""" - self._synchronized_data = synchronized_data - self.block_confirmations = 0 - self._previous_round_payload_class = previous_round_payload_class - self.context = context - - @classmethod - def auto_round_id(cls) -> str: - """ - Get round id automatically. - - This method returns the auto generated id from the class name if the - class variable behaviour_id is not set on the child class. - Otherwise, it returns the class variable behaviour_id. - """ - return ( - cls.round_id - if isinstance(cls.round_id, str) - else cls.__pattern.sub("_", cls.__name__).lower() - ) - - @property # type: ignore - def round_id(self) -> str: - """Get round id.""" - return self.auto_round_id() - - @property - def synchronized_data(self) -> BaseSynchronizedData: - """Get the synchronized data.""" - return self._synchronized_data - - def check_transaction(self, transaction: Transaction) -> None: - """ - Check transaction against the current state. - - :param transaction: the transaction - """ - self.check_payload_type(transaction) - self.check_payload(transaction.payload) - - def process_transaction(self, transaction: Transaction) -> None: - """ - Process a transaction. - - By convention, the payload handler should be a method - of the class that is named '{payload_name}'. - - :param transaction: the transaction. - """ - self.check_payload_type(transaction) - self.process_payload(transaction.payload) - - @abstractmethod - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: - """ - Process the end of the block. - - The role of this method is check whether the round - is considered ended. - - If the round is ended, the return value is - - the final result of the round. - - the event that triggers a transition. If None, the period - in which the round was executed is considered ended. - - This is done after each block because we consider the consensus engine's - block, and not the transaction, as the smallest unit - on which the consensus is reached; in other words, - each read operation on the state should be done - only after each block, and not after each transaction. - """ - - def check_payload_type(self, transaction: Transaction) -> None: - """ - Check the transaction is of the allowed transaction type. - - :param transaction: the transaction - :raises: TransactionTypeNotRecognizedError if the transaction can be - applied to the current state. - """ - if self.payload_class is None: - raise TransactionTypeNotRecognizedError( - "current round does not allow transactions" - ) - - payload_class = type(transaction.payload) - - if payload_class is self._previous_round_payload_class: - raise LateArrivingTransaction( - f"request '{transaction.payload}' is from previous round; skipping" - ) - - if payload_class is not self.payload_class: - raise TransactionTypeNotRecognizedError( - f"request '{payload_class}' not recognized; only {self.payload_class} is supported" - ) - - def check_majority_possible_with_new_voter( - self, - votes_by_participant: Dict[str, BaseTxPayload], - new_voter: str, - new_vote: BaseTxPayload, - nb_participants: int, - exception_cls: Type[ABCIAppException] = ABCIAppException, - ) -> None: - """ - Check that a Byzantine majority is achievable, once a new vote is added. - - :param votes_by_participant: a mapping from a participant to its vote, - before the new vote is added - :param new_voter: the new voter - :param new_vote: the new vote - :param nb_participants: the total number of participants - :param exception_cls: the class of the exception to raise in case the - check fails. - :raises: exception_cls: in case the check does not pass. - """ - # check preconditions - enforce( - new_voter not in votes_by_participant, - "voter has already voted", - ABCIAppInternalError, - ) - enforce( - len(votes_by_participant) <= nb_participants - 1, - "nb_participants not consistent with votes_by_participants", - ABCIAppInternalError, - ) - - # copy the input dictionary to avoid side effects - votes_by_participant = copy(votes_by_participant) - - # add the new vote - votes_by_participant[new_voter] = new_vote - - self.check_majority_possible( - votes_by_participant, nb_participants, exception_cls=exception_cls - ) - - def check_majority_possible( - self, - votes_by_participant: Dict[str, BaseTxPayload], - nb_participants: int, - exception_cls: Type[ABCIAppException] = ABCIAppException, - ) -> None: - """ - Check that a Byzantine majority is still achievable. - - The idea is that, even if all the votes have not been delivered yet, - it can be deduced whether a quorum cannot be reached due to - divergent preferences among the voters and due to a too small - number of other participants whose vote has not been delivered yet. - - The check fails iff: - - nb_remaining_votes + largest_nb_votes < quorum - - That is, if the number of remaining votes is not enough to make - the most voted item so far to exceed the quorum. - - Preconditions on the input: - - the size of votes_by_participant should not be greater than - "nb_participants - 1" voters - - new voter must not be in the current votes_by_participant - - :param votes_by_participant: a mapping from a participant to its vote - :param nb_participants: the total number of participants - :param exception_cls: the class of the exception to raise in case the - check fails. - :raises exception_cls: in case the check does not pass. - """ - enforce( - nb_participants > 0 and len(votes_by_participant) <= nb_participants, - "nb_participants not consistent with votes_by_participants", - ABCIAppInternalError, - ) - if len(votes_by_participant) == 0: - return - - votes = votes_by_participant.values() - vote_count = Counter(tuple(sorted(v.data.items())) for v in votes) - largest_nb_votes = max(vote_count.values()) - nb_votes_received = sum(vote_count.values()) - nb_remaining_votes = nb_participants - nb_votes_received - - if ( - nb_remaining_votes + largest_nb_votes - < self.synchronized_data.consensus_threshold - ): - raise exception_cls( - f"cannot reach quorum={self.synchronized_data.consensus_threshold}, " - f"number of remaining votes={nb_remaining_votes}, number of most voted item's votes={largest_nb_votes}" - ) - - def is_majority_possible( - self, votes_by_participant: Dict[str, BaseTxPayload], nb_participants: int - ) -> bool: - """ - Return true if a Byzantine majority is achievable, false otherwise. - - :param votes_by_participant: a mapping from a participant to its vote - :param nb_participants: the total number of participants - :return: True if the majority is still possible, false otherwise. - """ - try: - self.check_majority_possible(votes_by_participant, nb_participants) - except ABCIAppException: - return False - return True - - @abstractmethod - def check_payload(self, payload: BaseTxPayload) -> None: - """Check payload.""" - - @abstractmethod - def process_payload(self, payload: BaseTxPayload) -> None: - """Process payload.""" - - -class DegenerateRound(AbstractRound, ABC): - """ - This class represents the finished round during operation. - - It is a sink round. - """ - - payload_class = None - synchronized_data_class = BaseSynchronizedData - - def check_payload(self, payload: BaseTxPayload) -> None: - """Check payload.""" - raise NotImplementedError( # pragma: nocover - "DegenerateRound should not be used in operation." - ) - - def process_payload(self, payload: BaseTxPayload) -> None: - """Process payload.""" - raise NotImplementedError( # pragma: nocover - "DegenerateRound should not be used in operation." - ) - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: - """End block.""" - raise NotImplementedError( # pragma: nocover - "DegenerateRound should not be used in operation." - ) - - -class CollectionRound(AbstractRound, ABC): - """ - CollectionRound. - - This class represents abstract logic for collection based rounds where - the round object needs to collect data from different agents. The data - might for example be from a voting round or estimation round. - - `_allow_rejoin_payloads` is used to allow agents not currently active to - deliver a payload. - """ - - _allow_rejoin_payloads: bool = False - - def __init__(self, *args: Any, **kwargs: Any): - """Initialize the collection round.""" - super().__init__(*args, **kwargs) - self.collection: Dict[str, BaseTxPayload] = {} - - @staticmethod - def serialize_collection( - collection: DeserializedCollection, - ) -> SerializedCollection: - """Deserialize a serialized collection.""" - return {address: payload.json for address, payload in collection.items()} - - @staticmethod - def deserialize_collection( - serialized: SerializedCollection, - ) -> DeserializedCollection: - """Deserialize a serialized collection.""" - return { - address: BaseTxPayload.from_json(payload_json) - for address, payload_json in serialized.items() - } - - @property - def serialized_collection(self) -> SerializedCollection: - """A collection with the addresses mapped to serialized payloads.""" - return self.serialize_collection(self.collection) - - @property - def accepting_payloads_from(self) -> FrozenSet[str]: - """Accepting from the active set, or also from (re)joiners""" - if self._allow_rejoin_payloads: - return self.synchronized_data.all_participants - return self.synchronized_data.participants - - @property - def payloads(self) -> List[BaseTxPayload]: - """Get all agent payloads""" - return list(self.collection.values()) - - @property - def payload_values_count(self) -> Counter: - """Get count of payload values.""" - return Counter(map(lambda p: p.values, self.payloads)) - - def process_payload(self, payload: BaseTxPayload) -> None: - """Process payload.""" - if payload.round_count != self.synchronized_data.round_count: - raise ABCIAppInternalError( - f"Expected round count {self.synchronized_data.round_count} and got {payload.round_count}." - ) - - sender = payload.sender - if sender not in self.accepting_payloads_from: - raise ABCIAppInternalError( - f"{sender} not in list of participants: {sorted(self.accepting_payloads_from)}" - ) - - if sender in self.collection: - raise ABCIAppInternalError( - f"sender {sender} has already sent value for round: {self.round_id}" - ) - - self.collection[sender] = payload - - def check_payload(self, payload: BaseTxPayload) -> None: - """Check Payload""" - - # NOTE: the TransactionNotValidError is intercepted in ABCIRoundHandler.deliver_tx - # which means it will be logged instead of raised - if payload.round_count != self.synchronized_data.round_count: - raise TransactionNotValidError( - f"Expected round count {self.synchronized_data.round_count} and got {payload.round_count}." - ) - - sender_in_participant_set = payload.sender in self.accepting_payloads_from - if not sender_in_participant_set: - raise TransactionNotValidError( - f"{payload.sender} not in list of participants: {sorted(self.accepting_payloads_from)}" - ) - - if payload.sender in self.collection: - raise TransactionNotValidError( - f"sender {payload.sender} has already sent value for round: {self.round_id}" - ) - - -class _CollectUntilAllRound(CollectionRound, ABC): - """ - _CollectUntilAllRound - - This class represents abstract logic for when rounds need to collect payloads from all agents. - - This round should only be used when non-BFT behaviour is acceptable. - """ - - def check_payload(self, payload: BaseTxPayload) -> None: - """Check Payload""" - if payload.round_count != self.synchronized_data.round_count: - raise TransactionNotValidError( - f"Expected round count {self.synchronized_data.round_count} and got {payload.round_count}." - ) - - if payload.sender in self.collection: - raise TransactionNotValidError( - f"sender {payload.sender} has already sent value for round: {self.round_id}" - ) - - def process_payload(self, payload: BaseTxPayload) -> None: - """Process payload.""" - try: - self.check_payload(payload) - except TransactionNotValidError as e: - raise ABCIAppInternalError(e.args[0]) from e - - self.collection[payload.sender] = payload - - @property - def collection_threshold_reached( - self, - ) -> bool: - """Check that the collection threshold has been reached.""" - return len(self.collection) >= self.synchronized_data.max_participants - - -class CollectDifferentUntilAllRound(_CollectUntilAllRound, ABC): - """ - CollectDifferentUntilAllRound - - This class represents logic for rounds where a round needs to collect - different payloads from each agent. - - This round should only be used for registration of new agents when there is synchronization of the db. - """ - - def check_payload(self, payload: BaseTxPayload) -> None: - """Check Payload""" - new = payload.values - existing = [payload_.values for payload_ in self.collection.values()] - - if payload.sender not in self.collection and new in existing: - raise TransactionNotValidError( - f"`CollectDifferentUntilAllRound` encountered a value '{new}' that already exists. " - f"All values: {existing}" - ) - - super().check_payload(payload) - - -class CollectSameUntilAllRound(_CollectUntilAllRound, ABC): - """ - This class represents logic for when a round needs to collect the same payload from all the agents. - - This round should only be used for registration of new agents when there is no synchronization of the db. - """ - - def check_payload(self, payload: BaseTxPayload) -> None: - """Check Payload""" - new = payload.values - existing_ = [payload_.values for payload_ in self.collection.values()] - - if ( - payload.sender not in self.collection - and len(self.collection) - and new not in existing_ - ): - raise TransactionNotValidError( - f"`CollectSameUntilAllRound` encountered a value '{new}' " - f"which is not the same as the already existing one: '{existing_[0]}'" - ) - - super().check_payload(payload) - - @property - def common_payload( - self, - ) -> Any: - """Get the common payload among the agents.""" - return self.common_payload_values[0] - - @property - def common_payload_values( - self, - ) -> Tuple[Any, ...]: - """Get the common payload among the agents.""" - most_common_payload_values, max_votes = self.payload_values_count.most_common( - 1 - )[0] - if max_votes < self.synchronized_data.max_participants: - raise ABCIAppInternalError( - f"{max_votes} votes are not enough for `CollectSameUntilAllRound`. Expected: " - f"`n_votes = max_participants = {self.synchronized_data.max_participants}`" - ) - return most_common_payload_values - - -class CollectSameUntilThresholdRound(CollectionRound, ABC): - """ - CollectSameUntilThresholdRound - - This class represents logic for rounds where a round needs to collect - same payload from k of n agents. - - `done_event` is emitted when a) the collection threshold (k of n) is reached, - and b) the most voted payload has non-empty attributes. In this case all - payloads are saved under `collection_key` and the most voted payload attributes - are saved under `selection_key`. - - `none_event` is emitted when a) the collection threshold (k of n) is reached, - and b) the most voted payload has only empty attributes. - - `no_majority_event` is emitted when it is impossible to reach a k of n majority. - """ - - done_event: Any - no_majority_event: Any - none_event: Any - collection_key: str - selection_key: Union[str, Tuple[str, ...]] - - @property - def threshold_reached( - self, - ) -> bool: - """Check if the threshold has been reached.""" - counts = self.payload_values_count.values() - return any( - count >= self.synchronized_data.consensus_threshold for count in counts - ) - - @property - def most_voted_payload( - self, - ) -> Any: - """ - Get the most voted payload value. - - Kept for backward compatibility. - """ - return self.most_voted_payload_values[0] - - @property - def most_voted_payload_values( - self, - ) -> Tuple[Any, ...]: - """Get the most voted payload values.""" - most_voted_payload_values, max_votes = self.payload_values_count.most_common()[ - 0 - ] - if max_votes < self.synchronized_data.consensus_threshold: - raise ABCIAppInternalError("not enough votes") - return most_voted_payload_values - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: - """Process the end of the block.""" - if self.threshold_reached and any( - [val is not None for val in self.most_voted_payload_values] - ): - if isinstance(self.selection_key, tuple): - data = dict(zip(self.selection_key, self.most_voted_payload_values)) - data[self.collection_key] = self.serialized_collection - else: - data = { - self.collection_key: self.serialized_collection, - self.selection_key: self.most_voted_payload, - } - synchronized_data = self.synchronized_data.update( - synchronized_data_class=self.synchronized_data_class, - **data, - ) - return synchronized_data, self.done_event - if self.threshold_reached and not any( - [val is not None for val in self.most_voted_payload_values] - ): - return self.synchronized_data, self.none_event - if not self.is_majority_possible( - self.collection, self.synchronized_data.nb_participants - ): - return self.synchronized_data, self.no_majority_event - return None - - -class OnlyKeeperSendsRound(AbstractRound, ABC): - """ - OnlyKeeperSendsRound - - This class represents logic for rounds where only one agent sends a - payload. - - `done_event` is emitted when a) the keeper payload has been received and b) - the keeper payload has non-empty attributes. In this case all attributes are saved - under `payload_key`. - - `fail_event` is emitted when a) the keeper payload has been received and b) - the keeper payload has only empty attributes - """ - - keeper_payload: Optional[BaseTxPayload] = None - done_event: Any - fail_event: Any - payload_key: Union[str, Tuple[str, ...]] - - def process_payload(self, payload: BaseTxPayload) -> None: - """Handle a deploy safe payload.""" - if payload.round_count != self.synchronized_data.round_count: - raise ABCIAppInternalError( - f"Expected round count {self.synchronized_data.round_count} and got {payload.round_count}." - ) - - sender = payload.sender - - if sender not in self.synchronized_data.participants: - raise ABCIAppInternalError( - f"{sender} not in list of participants: {sorted(self.synchronized_data.participants)}" - ) - - if sender != self.synchronized_data.most_voted_keeper_address: - raise ABCIAppInternalError(f"{sender} not elected as keeper.") - - if self.keeper_payload is not None: - raise ABCIAppInternalError("keeper already set the payload.") - - self.keeper_payload = payload - - def check_payload(self, payload: BaseTxPayload) -> None: - """Check a deploy safe payload can be applied to the current state.""" - if payload.round_count != self.synchronized_data.round_count: - raise TransactionNotValidError( - f"Expected round count {self.synchronized_data.round_count} and got {payload.round_count}." - ) - - sender = payload.sender - sender_in_participant_set = sender in self.synchronized_data.participants - if not sender_in_participant_set: - raise TransactionNotValidError( - f"{sender} not in list of participants: {sorted(self.synchronized_data.participants)}" - ) - - sender_is_elected_sender = ( - sender == self.synchronized_data.most_voted_keeper_address - ) - if not sender_is_elected_sender: - raise TransactionNotValidError(f"{sender} not elected as keeper.") - - if self.keeper_payload is not None: - raise TransactionNotValidError("keeper payload value already set.") - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: - """Process the end of the block.""" - if self.keeper_payload is not None and any( - [val is not None for val in self.keeper_payload.values] - ): - if isinstance(self.payload_key, tuple): - data = dict(zip(self.payload_key, self.keeper_payload.values)) - else: - data = { - self.payload_key: self.keeper_payload.values[0], - } - synchronized_data = self.synchronized_data.update( - synchronized_data_class=self.synchronized_data_class, - **data, - ) - return synchronized_data, self.done_event - if self.keeper_payload is not None and not any( - [val is not None for val in self.keeper_payload.values] - ): - return self.synchronized_data, self.fail_event - return None - - -class VotingRound(CollectionRound, ABC): - """ - VotingRound - - This class represents logic for rounds where a round needs votes from - agents. Votes are in the form of `True` (positive), `False` (negative) - and `None` (abstain). The round ends when k of n agents make the same vote. - - `done_event` is emitted when a) the collection threshold (k of n) is reached - with k positive votes. In this case all payloads are saved under `collection_key`. - - `negative_event` is emitted when a) the collection threshold (k of n) is reached - with k negative votes. - - `none_event` is emitted when a) the collection threshold (k of n) is reached - with k abstain votes. - - `no_majority_event` is emitted when it is impossible to reach a k of n majority for - either of the options. - """ - - done_event: Any - negative_event: Any - none_event: Any - no_majority_event: Any - collection_key: str - - @property - def vote_count(self) -> Counter: - """Get agent payload vote count""" - - def parse_payload(payload: Any) -> Optional[bool]: - if not hasattr(payload, "vote"): - raise ValueError(f"payload {payload} has no attribute `vote`") - return payload.vote - - return Counter(parse_payload(payload) for payload in self.collection.values()) - - @property - def positive_vote_threshold_reached(self) -> bool: - """Check that the vote threshold has been reached.""" - return self.vote_count[True] >= self.synchronized_data.consensus_threshold - - @property - def negative_vote_threshold_reached(self) -> bool: - """Check that the vote threshold has been reached.""" - return self.vote_count[False] >= self.synchronized_data.consensus_threshold - - @property - def none_vote_threshold_reached(self) -> bool: - """Check that the vote threshold has been reached.""" - return self.vote_count[None] >= self.synchronized_data.consensus_threshold - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: - """Process the end of the block.""" - if self.positive_vote_threshold_reached: - synchronized_data = self.synchronized_data.update( - synchronized_data_class=self.synchronized_data_class, - **{self.collection_key: self.serialized_collection}, - ) - return synchronized_data, self.done_event - if self.negative_vote_threshold_reached: - return self.synchronized_data, self.negative_event - if self.none_vote_threshold_reached: - return self.synchronized_data, self.none_event - if not self.is_majority_possible( - self.collection, self.synchronized_data.nb_participants - ): - return self.synchronized_data, self.no_majority_event - return None - - -class CollectDifferentUntilThresholdRound(CollectionRound, ABC): - """ - CollectDifferentUntilThresholdRound - - This class represents logic for rounds where a round needs to collect - different payloads from k of n agents. - - `done_event` is emitted when a) the required block confirmations - have been met, and b) the collection threshold (k of n) is reached. In - this case all payloads are saved under `collection_key`. - - Extended `required_block_confirmations` to allow for arrival of more - payloads. - """ - - done_event: Any - collection_key: str - required_block_confirmations: int = 0 - - @property - def collection_threshold_reached( - self, - ) -> bool: - """Check if the threshold has been reached.""" - return len(self.collection) >= self.synchronized_data.consensus_threshold - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: - """Process the end of the block.""" - if self.collection_threshold_reached: - self.block_confirmations += 1 - if ( - self.collection_threshold_reached - and self.block_confirmations > self.required_block_confirmations - ): - synchronized_data = self.synchronized_data.update( - synchronized_data_class=self.synchronized_data_class, - **{ - self.collection_key: self.serialized_collection, - }, - ) - return synchronized_data, self.done_event - - return None - - -class CollectNonEmptyUntilThresholdRound(CollectDifferentUntilThresholdRound, ABC): - """ - CollectNonEmptyUntilThresholdRound - - This class represents logic for rounds where a round needs to collect - optionally different payloads from k of n agents, where we only keep the non-empty attributes. - - `done_event` is emitted when a) the required block confirmations - have been met, b) the collection threshold (k of n) is reached, and - c) some non-empty attribute values have been collected. In this case - all payloads are saved under `collection_key`. Under `selection_key` - the non-empty attribute values are stored. - - `none_event` is emitted when a) the required block confirmations - have been met, b) the collection threshold (k of n) is reached, and - c) no non-empty attribute values have been collected. - - Attention: A `none_event` might be triggered even though some of the - remaining n-k agents might send non-empty attributes! Extended - `required_block_confirmations` can alleviate this somewhat. - """ - - none_event: Any - selection_key: Union[str, Tuple[str, ...]] - - def _get_non_empty_values(self) -> Dict[str, Tuple[Any, ...]]: - """Get the non-empty values from the payload, for all attributes.""" - non_empty_values: Dict[str, List[List[Any]]] = {} - - for sender, payload in self.collection.items(): - if sender not in non_empty_values: - non_empty_values[sender] = [ - value for value in payload.values if value is not None - ] - if len(non_empty_values[sender]) == 0: - del non_empty_values[sender] - continue - non_empty_values_ = { - sender: tuple(li) for sender, li in non_empty_values.items() - } - return non_empty_values_ - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: - """Process the end of the block.""" - if self.collection_threshold_reached: - self.block_confirmations += 1 - if ( - self.collection_threshold_reached - and self.block_confirmations > self.required_block_confirmations - ): - non_empty_values = self._get_non_empty_values() - - if isinstance(self.selection_key, tuple): - data: Dict[str, Any] = { - sender: dict(zip(self.selection_key, values)) - for sender, values in non_empty_values.items() - } - else: - data = { - self.selection_key: { - sender: values[0] for sender, values in non_empty_values.items() - }, - } - data[self.collection_key] = self.serialized_collection - - synchronized_data = self.synchronized_data.update( - synchronized_data_class=self.synchronized_data_class, - **data, - ) - - if all([len(tu) == 0 for tu in non_empty_values]): - return self.synchronized_data, self.none_event - return synchronized_data, self.done_event - return None - - -AppState = Type[AbstractRound] -AbciAppTransitionFunction = Dict[AppState, Dict[EventType, AppState]] -EventToTimeout = Dict[EventType, float] - - -@dataclass(order=True) -class TimeoutEvent(Generic[EventType]): - """Timeout event.""" - - deadline: datetime.datetime - entry_count: int - event: EventType = field(compare=False) - cancelled: bool = field(default=False, compare=False) - - -class Timeouts(Generic[EventType]): - """Class to keep track of pending timeouts.""" - - def __init__(self) -> None: - """Initialize.""" - # The entry count serves as a tie-breaker so that two tasks with - # the same priority are returned in the order they were added - self._counter = itertools.count() - - # The timeout priority queue keeps the earliest deadline at the top. - self._heap: List[TimeoutEvent[EventType]] = [] - - # Mapping from entry id to task - self._entry_finder: Dict[int, TimeoutEvent[EventType]] = {} - - @property - def size(self) -> int: - """Get the size of the timeout queue.""" - return len(self._heap) - - def add_timeout(self, deadline: datetime.datetime, event: EventType) -> int: - """Add a timeout.""" - entry_count = next(self._counter) - timeout_event = TimeoutEvent[EventType](deadline, entry_count, event) - heapq.heappush(self._heap, timeout_event) - self._entry_finder[entry_count] = timeout_event - return entry_count - - def cancel_timeout(self, entry_count: int) -> None: - """ - Remove a timeout. - - :param entry_count: the entry id to remove. - :raises: KeyError: if the entry count is not found. - """ - if entry_count in self._entry_finder: - self._entry_finder[entry_count].cancelled = True - - def pop_earliest_cancelled_timeouts(self) -> None: - """Pop earliest cancelled timeouts.""" - if self.size == 0: - return - entry = self._heap[0] # heap peak - while entry.cancelled: - self.pop_timeout() - if self.size == 0: - break - entry = self._heap[0] - - def get_earliest_timeout(self) -> Tuple[datetime.datetime, Any]: - """Get the earliest timeout-event pair.""" - entry = self._heap[0] - return entry.deadline, entry.event - - def pop_timeout(self) -> Tuple[datetime.datetime, Any]: - """Remove and return the earliest timeout-event pair.""" - entry = heapq.heappop(self._heap) - del self._entry_finder[entry.entry_count] - return entry.deadline, entry.event - - -class _MetaAbciApp(ABCMeta): - """A metaclass that validates AbciApp's attributes.""" - - bg_round_added: bool = False - - def __new__(mcs, name: str, bases: Tuple, namespace: Dict, **kwargs: Any) -> Type: # type: ignore - """Initialize the class.""" - new_cls = super().__new__(mcs, name, bases, namespace, **kwargs) - - if ABC in bases: - # abstract class, return - return new_cls - if not issubclass(new_cls, AbciApp): - # the check only applies to AbciApp subclasses - return new_cls - - if not mcs.bg_round_added: - mcs._add_pending_offences_bg_round(new_cls) - mcs.bg_round_added = True - - mcs._check_consistency(cast(Type[AbciApp], new_cls)) - - return new_cls - - @classmethod - def _check_consistency(mcs, abci_app_cls: Type["AbciApp"]) -> None: - """Check consistency of class attributes.""" - mcs._check_required_class_attributes(abci_app_cls) - mcs._check_initial_states_and_final_states(abci_app_cls) - mcs._check_consistency_outgoing_transitions_from_non_final_states(abci_app_cls) - mcs._check_db_constraints_consistency(abci_app_cls) - - @classmethod - def _check_required_class_attributes(mcs, abci_app_cls: Type["AbciApp"]) -> None: - """Check that required class attributes are set.""" - if not hasattr(abci_app_cls, "initial_round_cls"): - raise ABCIAppInternalError("'initial_round_cls' field not set") - if not hasattr(abci_app_cls, "transition_function"): - raise ABCIAppInternalError("'transition_function' field not set") - - @classmethod - def _check_initial_states_and_final_states( - mcs, - abci_app_cls: Type["AbciApp"], - ) -> None: - """ - Check that initial states and final states are consistent. - - I.e.: - - check that all the initial states are in the set of states specified - by the transition function. - - check that the initial state has outgoing transitions - - check that the initial state does not trigger timeout events. This is - because we need at least one block/timestamp to start timeouts. - - check that initial states are not final states. - - check that the set of final states is a proper subset of the set of - states. - - check that a final state does not have outgoing transitions. - - :param abci_app_cls: the AbciApp class - """ - initial_round_cls = abci_app_cls.initial_round_cls - initial_states = abci_app_cls.initial_states - transition_function = abci_app_cls.transition_function - final_states = abci_app_cls.final_states - states = abci_app_cls.get_all_rounds() - - enforce( - initial_states == set() or initial_round_cls in initial_states, - f"initial round class {initial_round_cls} is not in the set of " - f"initial states: {initial_states}", - ) - enforce( - initial_round_cls in states - and all(initial_state in states for initial_state in initial_states), - "initial states must be in the set of states", - ) - - true_initial_states = ( - initial_states if initial_states != set() else {initial_round_cls} - ) - enforce( - all( - initial_state not in final_states - for initial_state in true_initial_states - ), - "initial states cannot be final states", - ) - - unknown_final_states = set.difference(final_states, states) - enforce( - len(unknown_final_states) == 0, - f"the following final states are not in the set of states:" - f" {unknown_final_states}", - ) - - enforce( - all( - len(transition_function[final_state]) == 0 - for final_state in final_states - ), - "final states cannot have outgoing transitions", - ) - - enforce( - all( - issubclass(final_state, DegenerateRound) for final_state in final_states - ), - "final round classes must be subclasses of the DegenerateRound class", - ) - - @classmethod - def _check_db_constraints_consistency(mcs, abci_app_cls: Type["AbciApp"]) -> None: - """Check that the pre and post conditions on the db are consistent with the initial and final states.""" - expected = abci_app_cls.initial_states - actual = abci_app_cls.db_pre_conditions.keys() - is_pre_conditions_set = len(actual) != 0 - invalid_initial_states = ( - set.difference(expected, actual) if is_pre_conditions_set else set() - ) - enforce( - len(invalid_initial_states) == 0, - f"db pre conditions contain invalid initial states: {invalid_initial_states}", - ) - expected = abci_app_cls.final_states - actual = abci_app_cls.db_post_conditions.keys() - is_post_conditions_set = len(actual) != 0 - invalid_final_states = ( - set.difference(expected, actual) if is_post_conditions_set else set() - ) - enforce( - len(invalid_final_states) == 0, - f"db post conditions contain invalid final states: {invalid_final_states}", - ) - all_pre_conditions = { - value - for values in abci_app_cls.db_pre_conditions.values() - for value in values - } - all_post_conditions = { - value - for values in abci_app_cls.db_post_conditions.values() - for value in values - } - enforce( - len(all_pre_conditions.intersection(all_post_conditions)) == 0, - "db pre and post conditions intersect", - ) - intersection = abci_app_cls.default_db_preconditions.intersection( - all_pre_conditions - ) - enforce( - len(intersection) == 0, - f"db pre conditions contain value that is a default pre condition: {intersection}", - ) - intersection = abci_app_cls.default_db_preconditions.intersection( - all_post_conditions - ) - enforce( - len(intersection) == 0, - f"db post conditions contain value that is a default post condition: {intersection}", - ) - - @classmethod - def _check_consistency_outgoing_transitions_from_non_final_states( - mcs, abci_app_cls: Type["AbciApp"] - ) -> None: - """ - Check consistency of outgoing transitions from non-final states. - - In particular, check that all non-final states have: - - at least one non-timeout transition. - - at most one timeout transition - - :param abci_app_cls: the AbciApp class - """ - states = abci_app_cls.get_all_rounds() - event_to_timeout = abci_app_cls.event_to_timeout - - non_final_states = states.difference(abci_app_cls.final_states) - timeout_events = set(event_to_timeout.keys()) - for non_final_state in non_final_states: - outgoing_transitions = abci_app_cls.transition_function[non_final_state] - - outgoing_events = set(outgoing_transitions.keys()) - outgoing_timeout_events = set.intersection(outgoing_events, timeout_events) - outgoing_nontimeout_events = set.difference(outgoing_events, timeout_events) - - enforce( - len(outgoing_timeout_events) < 2, - f"non-final state {non_final_state} cannot have more than one " - f"outgoing timeout event, got: " - f"{', '.join(map(str, outgoing_timeout_events))}", - ) - enforce( - len(outgoing_nontimeout_events) > 0, - f"non-final state {non_final_state} must have at least one " - f"non-timeout transition", - ) - - @classmethod - def _add_pending_offences_bg_round(cls, abci_app_cls: Type["AbciApp"]) -> None: - """Add the pending offences synchronization background round.""" - config: BackgroundAppConfig = BackgroundAppConfig(PendingOffencesRound) - abci_app_cls.add_background_app(config) - - -class BackgroundAppType(Enum): - """ - The type of a background app. - - Please note that the values correspond to the priority in which the background apps should be processed - when updating rounds. - """ - - TERMINATING = 0 - EVER_RUNNING = 1 - NORMAL = 2 - INCORRECT = 3 - - @staticmethod - def correct_types() -> Set[str]: - """Return the correct types only.""" - return set(BackgroundAppType.__members__) - {BackgroundAppType.INCORRECT.name} - - -@dataclass(frozen=True) -class BackgroundAppConfig(Generic[EventType]): - """ - Necessary configuration for a background app. - - For a deeper understanding of the various types of background apps and how the config influences - the generated background app's type, please refer to the `BackgroundApp` class. - The `specify_type` method provides further insight on the subject matter. - """ - - # the class of the background round - round_cls: AppState - # the abci app of the background round - # the abci app must specify a valid transition function if the round is not of an ever-running type - abci_app: Optional[Type["AbciApp"]] = None - # the start event of the background round - # if no event or transition function is specified, then the round is running in the background forever - start_event: Optional[EventType] = None - # the end event of the background round - # if not specified, then the round is terminating the abci app - end_event: Optional[EventType] = None - - -class BackgroundApp(Generic[EventType]): - """A background app.""" - - def __init__( - self, - config: BackgroundAppConfig, - ) -> None: - """Initialize the BackgroundApp.""" - given_args = locals() - - self.config = config - self.round_cls: AppState = config.round_cls - self.transition_function: Optional[AbciAppTransitionFunction] = ( - config.abci_app.transition_function if config.abci_app is not None else None - ) - self.start_event: Optional[EventType] = config.start_event - self.end_event: Optional[EventType] = config.end_event - - self.type = self.specify_type() - if self.type == BackgroundAppType.INCORRECT: # pragma: nocover - raise ValueError( - f"Background app has not been initialized correctly with {given_args}. " - f"Cannot match with any of the possible background apps' types: {BackgroundAppType.correct_types()}" - ) - _logger.debug( - f"Created background app of type '{self.type}' using {given_args}." - ) - self._background_round: Optional[AbstractRound] = None - - def __eq__(self, other: Any) -> bool: # pragma: no cover - """Custom equality comparing operator.""" - if not isinstance(other, BackgroundApp): - return False - - return self.config == other.config - - def __hash__(self) -> int: - """Custom hashing operator""" - return hash(self.config) - - def specify_type(self) -> BackgroundAppType: - """Specify the type of the background app.""" - if ( - self.start_event is None - and self.end_event is None - and self.transition_function is None - ): - self.transition_function = {} - return BackgroundAppType.EVER_RUNNING - if ( - self.start_event is not None - and self.end_event is None - and self.transition_function is not None - ): - return BackgroundAppType.TERMINATING - if ( - self.start_event is not None - and self.end_event is not None - and self.transition_function is not None - ): - return BackgroundAppType.NORMAL - return BackgroundAppType.INCORRECT # pragma: nocover - - def setup( - self, initial_synchronized_data: BaseSynchronizedData, context: SkillContext - ) -> None: - """Set up the background round.""" - round_cls = cast(Type[AbstractRound], self.round_cls) - self._background_round = round_cls( - initial_synchronized_data, - context, - ) - - @property - def background_round(self) -> AbstractRound: - """Get the background round.""" - if self._background_round is None: # pragma: nocover - raise ValueError(f"Background round with class `{self.round_cls}` not set!") - return self._background_round - - def process_transaction(self, transaction: Transaction, dry: bool = False) -> bool: - """Process a transaction.""" - - payload_class = type(transaction.payload) - bg_payload_class = cast(AppState, self.round_cls).payload_class - if payload_class is bg_payload_class: - processor = ( - self.background_round.check_transaction - if dry - else self.background_round.process_transaction - ) - processor(transaction) - return True - return False - - -@dataclass -class TransitionBackup: - """Holds transition related information as a backup in case we want to transition back from a background app.""" - - round: Optional[AbstractRound] = None - round_cls: Optional[AppState] = None - transition_function: Optional[AbciAppTransitionFunction] = None - - -class AbciApp( - Generic[EventType], ABC, metaclass=_MetaAbciApp -): # pylint: disable=too-many-instance-attributes - """ - Base class for ABCI apps. - - Concrete classes of this class implement the ABCI App. - """ - - initial_round_cls: AppState - initial_states: Set[AppState] = set() - transition_function: AbciAppTransitionFunction - final_states: Set[AppState] = set() - event_to_timeout: EventToTimeout = {} - cross_period_persisted_keys: FrozenSet[str] = frozenset() - background_apps: Set[BackgroundApp] = set() - default_db_preconditions: Set[str] = BaseSynchronizedData.default_db_keys - db_pre_conditions: Dict[AppState, Set[str]] = {} - db_post_conditions: Dict[AppState, Set[str]] = {} - _is_abstract: bool = True - - def __init__( - self, - synchronized_data: BaseSynchronizedData, - logger: logging.Logger, - context: SkillContext, - ): - """Initialize the AbciApp.""" - - synchronized_data_class = self.initial_round_cls.synchronized_data_class - synchronized_data = synchronized_data_class(db=synchronized_data.db) - - self._initial_synchronized_data = synchronized_data - self.logger = logger - self.context = context - self._current_round_cls: Optional[AppState] = None - self._current_round: Optional[AbstractRound] = None - self._last_round: Optional[AbstractRound] = None - self._previous_rounds: List[AbstractRound] = [] - self._current_round_height: int = 0 - self._round_results: List[BaseSynchronizedData] = [] - self._last_timestamp: Optional[datetime.datetime] = None - self._current_timeout_entries: List[int] = [] - self._timeouts = Timeouts[EventType]() - self._transition_backup = TransitionBackup() - self._switched = False - - @classmethod - def is_abstract(cls) -> bool: - """Return if the abci app is abstract.""" - return cls._is_abstract - - @classmethod - def add_background_app( - cls, - config: BackgroundAppConfig, - ) -> Type["AbciApp"]: - """ - Sets the background related class variables. - - For a deeper understanding of the various types of background apps and how the inputs of this method influence - the generated background app's type, please refer to the `BackgroundApp` class. - The `specify_type` method provides further insight on the subject matter. - - :param config: the background app's configuration. - :return: the `AbciApp` with the new background app contained in the `background_apps` set. - """ - background_app: BackgroundApp = BackgroundApp(config) - cls.background_apps.add(background_app) - cross_period_keys = ( - config.abci_app.cross_period_persisted_keys - if config.abci_app is not None - else frozenset() - ) - cls.cross_period_persisted_keys = cls.cross_period_persisted_keys.union( - cross_period_keys - ) - return cls - - @property - def synchronized_data(self) -> BaseSynchronizedData: - """Return the current synchronized data.""" - latest_result = self.latest_result or self._initial_synchronized_data - if self._current_round_cls is None: - return latest_result - synchronized_data_class = self._current_round_cls.synchronized_data_class - result = ( - synchronized_data_class(db=latest_result.db) - if isclass(synchronized_data_class) - and issubclass(synchronized_data_class, BaseSynchronizedData) - else latest_result - ) - return result - - @classmethod - def get_all_rounds(cls) -> Set[AppState]: - """Get all the round states.""" - return set(cls.transition_function) - - @classmethod - def get_all_events(cls) -> Set[EventType]: - """Get all the events.""" - events: Set[EventType] = set() - for _, transitions in cls.transition_function.items(): - events.update(transitions.keys()) - return events - - @staticmethod - def _get_rounds_from_transition_function( - transition_function: Optional[AbciAppTransitionFunction], - ) -> Set[AppState]: - """Get rounds from a transition function.""" - if transition_function is None: - return set() - result: Set[AppState] = set() - for start, transitions in transition_function.items(): - result.add(start) - result.update(transitions.values()) - return result - - @classmethod - def get_all_round_classes( - cls, - bg_round_cls: Set[Type[AbstractRound]], - include_background_rounds: bool = False, - ) -> Set[AppState]: - """Get all round classes.""" - full_fn = deepcopy(cls.transition_function) - - if include_background_rounds: - for app in cls.background_apps: - if ( - app.type != BackgroundAppType.EVER_RUNNING - and app.round_cls in bg_round_cls - ): - transition_fn = cast( - AbciAppTransitionFunction, app.transition_function - ) - full_fn.update(transition_fn) - - return cls._get_rounds_from_transition_function(full_fn) - - @property - def bg_apps_prioritized(self) -> Tuple[List[BackgroundApp], ...]: - """Get the background apps grouped and prioritized by their types.""" - n_correct_types = len(BackgroundAppType.correct_types()) - grouped_prioritized: Tuple[List, ...] = ([],) * n_correct_types - for app in self.background_apps: - # reminder: the values correspond to the priority of the background apps - for priority in range(n_correct_types): - if app.type == BackgroundAppType(priority): - grouped_prioritized[priority].append(app) - - return grouped_prioritized - - @property - def last_timestamp(self) -> datetime.datetime: - """Get last timestamp.""" - if self._last_timestamp is None: - raise ABCIAppInternalError("last timestamp is None") - return self._last_timestamp - - def _setup_background(self) -> None: - """Set up the background rounds.""" - for app in self.background_apps: - app.setup(self._initial_synchronized_data, self.context) - - def _get_synced_value( - self, - db_key: str, - sync_classes: Set[Type[BaseSynchronizedData]], - default: Any = None, - ) -> Any: - """Get the value of a specific database key using the synchronized data.""" - for cls in sync_classes: - # try to find the value using the synchronized data as suggested in #2131 - synced_data = cls(db=self.synchronized_data.db) - try: - res = getattr(synced_data, db_key) - except AttributeError: - # if the property does not exist in the db try the next synced data class - continue - except ValueError: - # if the property raised because of using `get_strict` and the key not being present in the db - break - - # if there is a property with the same name as the key in the db, return the result, normalized - return AbciAppDB.normalize(res) - - # as a last resort, try to get the value from the db - return self.synchronized_data.db.get(db_key, default) - - def setup(self) -> None: - """Set up the behaviour.""" - self.schedule_round(self.initial_round_cls) - self._setup_background() - # iterate through all the rounds and get all the unique synced data classes - sync_classes = { - _round.synchronized_data_class for _round in self.transition_function - } - # Add `BaseSynchronizedData` in case it does not exist (TODO: investigate and remove as it might always exist) - sync_classes.add(BaseSynchronizedData) - # set the cross-period persisted keys; avoid raising when the first period ends without a key in the db - update = { - db_key: self._get_synced_value(db_key, sync_classes) - for db_key in self.cross_period_persisted_keys - } - self.synchronized_data.db.update(**update) - - def _log_start(self) -> None: - """Log the entering in the round.""" - self.logger.info( - f"Entered in the '{self.current_round.round_id}' round for period " - f"{self.synchronized_data.period_count}" - ) - - def _log_end(self, event: EventType) -> None: - """Log the exiting from the round.""" - self.logger.info( - f"'{self.current_round.round_id}' round is done with event: {event}" - ) - - def _extend_previous_rounds_with_current_round(self) -> None: - self._previous_rounds.append(self.current_round) - self._current_round_height += 1 - - def schedule_round(self, round_cls: AppState) -> None: - """ - Schedule a round class. - - this means: - - cancel timeout events belonging to the current round; - - instantiate the new round class and set it as current round; - - create new timeout events and schedule them according to the latest - timestamp. - - :param round_cls: the class of the new round. - """ - self.logger.debug("scheduling new round: %s", round_cls) - for entry_id in self._current_timeout_entries: - self._timeouts.cancel_timeout(entry_id) - - self._current_timeout_entries = [] - next_events = list(self.transition_function.get(round_cls, {}).keys()) - for event in next_events: - timeout = self.event_to_timeout.get(event, None) - # if first round, last_timestamp is None. - # This means we do not schedule timeout events, - # but we allow timeout events from the initial state - # in case of concatenation. - if timeout is not None and self._last_timestamp is not None: - # last timestamp can be in the past relative to last seen block - # time if we're scheduling from within update_time - deadline = self.last_timestamp + datetime.timedelta(0, timeout) - entry_id = self._timeouts.add_timeout(deadline, event) - self.logger.debug( - "scheduling timeout of %s seconds for event %s with deadline %s", - timeout, - event, - deadline, - ) - self._current_timeout_entries.append(entry_id) - - self._last_round = self._current_round - self._current_round_cls = round_cls - self._current_round = round_cls( - self.synchronized_data, - self.context, - ( - self._last_round.payload_class - if self._last_round is not None - and self._last_round.payload_class - != self._current_round_cls.payload_class - # when transitioning to a round with the same payload type we set None - # as otherwise it will allow no tx to be submitted - else None - ), - ) - self._log_start() - self.synchronized_data.db.increment_round_count() # ROUND_COUNT_DEFAULT is -1 - - @property - def current_round(self) -> AbstractRound: - """Get the current round.""" - if self._current_round is None: - raise ValueError("current_round not set!") - return self._current_round - - @property - def current_round_id(self) -> Optional[str]: - """Get the current round id.""" - return self._current_round.round_id if self._current_round else None - - @property - def current_round_height(self) -> int: - """Get the current round height.""" - return self._current_round_height - - @property - def last_round_id(self) -> Optional[str]: - """Get the last round id.""" - return self._last_round.round_id if self._last_round else None - - @property - def is_finished(self) -> bool: - """Check whether the AbciApp execution has finished.""" - return self._current_round is None - - @property - def latest_result(self) -> Optional[BaseSynchronizedData]: - """Get the latest result of the round.""" - return None if len(self._round_results) == 0 else self._round_results[-1] - - def cleanup_timeouts(self) -> None: - """ - Remove all timeouts. - - Note that this is method is meant to be used only when performing recovery. - Calling it in normal execution will result in unexpected behaviour. - """ - self._timeouts = Timeouts[EventType]() - self._current_timeout_entries = [] - self._last_timestamp = None - - def check_transaction(self, transaction: Transaction) -> None: - """Check a transaction.""" - - self.process_transaction(transaction, dry=True) - - def process_transaction(self, transaction: Transaction, dry: bool = False) -> None: - """ - Process a transaction. - - The background rounds run concurrently with other (normal) rounds. - First we check if the transaction is meant for a background round, - if not we forward it to the current round object. - - :param transaction: the transaction. - :param dry: whether the transaction should only be checked and not processed. - """ - - for app in self.background_apps: - processed = app.process_transaction(transaction, dry) - if processed: - return - - processor = ( - self.current_round.check_transaction - if dry - else self.current_round.process_transaction - ) - processor(transaction) - - def _resolve_bg_transition( - self, app: BackgroundApp, event: EventType - ) -> Tuple[bool, Optional[AppState]]: - """ - Resolve a background app's transition. - - First check whether the event is a special start event. - If that's the case, proceed with the corresponding background app's transition function, - regardless of what the current round is. - - :param app: the background app instance. - :param event: the event for the transition. - :return: the new app state. - """ - - if ( - app.type in (BackgroundAppType.NORMAL, BackgroundAppType.TERMINATING) - and event == app.start_event - ): - app.transition_function = cast( - AbciAppTransitionFunction, app.transition_function - ) - app.round_cls = cast(AppState, app.round_cls) - next_round_cls = app.transition_function[app.round_cls].get(event, None) - if next_round_cls is None: # pragma: nocover - return True, None - - # we backup the current round so we can return back to normal, in case the end event is received later - self._transition_backup.round = self._current_round - self._transition_backup.round_cls = self._current_round_cls - # we switch the current transition function, with the background app's transition function - self._transition_backup.transition_function = deepcopy( - self.transition_function - ) - self.transition_function = app.transition_function - self.logger.info( - f"The {event} event was produced, transitioning to " - f"`{next_round_cls.auto_round_id()}`." - ) - return True, next_round_cls - - return False, None - - def _adjust_transition_fn(self, event: EventType) -> None: - """ - Adjust the transition function if necessary. - - Check whether the event is a special end event. - If that's the case, reset the transition function back to normal. - This method is meant to be called after resolving the next round transition, given an event. - - :param event: the emitted event. - """ - if self._transition_backup.transition_function is None: - return - - for app in self.background_apps: - if app.type == BackgroundAppType.NORMAL and event == app.end_event: - self._current_round = self._transition_backup.round - self._transition_backup.round = None - self._current_round_cls = self._transition_backup.round_cls - self._transition_backup.round_cls = None - backup_fn = cast( - AbciAppTransitionFunction, - self._transition_backup.transition_function, - ) - self.transition_function = deepcopy(backup_fn) - self._transition_backup.transition_function = None - self._switched = True - self.logger.info( - f"The {app.end_event} event was produced. Switching back to the normal FSM." - ) - - def _resolve_transition(self, event: EventType) -> Optional[Type[AbstractRound]]: - """Resolve the transitioning based on the given event.""" - for app in self.background_apps: - matched, next_round_cls = self._resolve_bg_transition(app, event) - if matched: - return next_round_cls - - self._adjust_transition_fn(event) - - current_round_cls = cast(AppState, self._current_round_cls) - next_round_cls = self.transition_function[current_round_cls].get(event, None) - if next_round_cls is None: - return None - - return next_round_cls - - def process_event( - self, event: EventType, result: Optional[BaseSynchronizedData] = None - ) -> None: - """Process a round event.""" - if self._current_round_cls is None: - self.logger.warning( - f"Cannot process event '{event}' as current state is not set" - ) - return - - next_round_cls = self._resolve_transition(event) - self._extend_previous_rounds_with_current_round() - # if there is no result, we duplicate the state since the round was preemptively ended - result = self.current_round.synchronized_data if result is None else result - self._round_results.append(result) - - self._log_end(event) - if next_round_cls is not None: - self.schedule_round(next_round_cls) - return - - if self._switched: - self._switched = False - return - - self.logger.warning("AbciApp has reached a dead end.") - self._current_round_cls = None - self._current_round = None - - def update_time(self, timestamp: datetime.datetime) -> None: - """ - Observe timestamp from last block. - - :param timestamp: the latest block's timestamp. - """ - self.logger.debug("arrived block with timestamp: %s", timestamp) - self.logger.debug("current AbciApp time: %s", self._last_timestamp) - self._timeouts.pop_earliest_cancelled_timeouts() - - if self._timeouts.size == 0: - # if no pending timeouts, then it is safe to - # move forward the last known timestamp to the - # latest block's timestamp. - self.logger.debug("no pending timeout, move time forward") - self._last_timestamp = timestamp - return - - earliest_deadline, _ = self._timeouts.get_earliest_timeout() - while earliest_deadline <= timestamp: - # the earliest deadline is expired. Pop it from the - # priority queue and process the timeout event. - expired_deadline, timeout_event = self._timeouts.pop_timeout() - self.logger.warning( - "expired deadline %s with event %s at AbciApp time %s", - expired_deadline, - timeout_event, - timestamp, - ) - - # the last timestamp now becomes the expired deadline - # clearly, it is earlier than the current highest known - # timestamp that comes from the consensus engine. - # However, we need it to correctly simulate the timeouts - # of the next rounds. (for now we set it to timestamp to explore - # the impact) - self._last_timestamp = timestamp - self.logger.warning( - "current AbciApp time after expired deadline: %s", self.last_timestamp - ) - - self.process_event(timeout_event) - - self._timeouts.pop_earliest_cancelled_timeouts() - if self._timeouts.size == 0: - break - earliest_deadline, _ = self._timeouts.get_earliest_timeout() - - # at this point, there is no timeout event left to be triggered, - # so it is safe to move forward the last known timestamp to the - # new block's timestamp - self._last_timestamp = timestamp - self.logger.debug("final AbciApp time: %s", self._last_timestamp) - - def cleanup( - self, - cleanup_history_depth: int, - cleanup_history_depth_current: Optional[int] = None, - ) -> None: - """Clear data.""" - if len(self._round_results) != len(self._previous_rounds): - raise ABCIAppInternalError("Inconsistent round lengths") # pragma: nocover - # we need at least the last round result, and for symmetry we impose the same condition - # on previous rounds and state.db - cleanup_history_depth = max(cleanup_history_depth, MIN_HISTORY_DEPTH) - self._previous_rounds = self._previous_rounds[-cleanup_history_depth:] - self._round_results = self._round_results[-cleanup_history_depth:] - self.synchronized_data.db.cleanup( - cleanup_history_depth, cleanup_history_depth_current - ) - - def cleanup_current_histories(self, cleanup_history_depth_current: int) -> None: - """Reset the parameter histories for the current entry (period), keeping only the latest values for each parameter.""" - self.synchronized_data.db.cleanup_current_histories( - cleanup_history_depth_current - ) - - -class OffenseType(Enum): - """ - The types of offenses. - - The values of the enum represent the seriousness of the offence. - Offense types with values >1000 are considered serious. - See also `is_light_offence` and `is_serious_offence` functions. - """ - - NO_OFFENCE = -2 - CUSTOM = -1 - VALIDATOR_DOWNTIME = 0 - INVALID_PAYLOAD = 1 - BLACKLISTED = 2 - SUSPECTED = 3 - UNKNOWN = SERIOUS_OFFENCE_ENUM_MIN - DOUBLE_SIGNING = SERIOUS_OFFENCE_ENUM_MIN + 1 - LIGHT_CLIENT_ATTACK = SERIOUS_OFFENCE_ENUM_MIN + 2 - - -def is_light_offence(offence_type: OffenseType) -> bool: - """Check if an offence type is light.""" - return 0 <= offence_type.value < SERIOUS_OFFENCE_ENUM_MIN - - -def is_serious_offence(offence_type: OffenseType) -> bool: - """Check if an offence type is serious.""" - return offence_type.value >= SERIOUS_OFFENCE_ENUM_MIN - - -def light_offences() -> Iterator[OffenseType]: - """Get the light offences.""" - return filter(is_light_offence, OffenseType) - - -def serious_offences() -> Iterator[OffenseType]: - """Get the serious offences.""" - return filter(is_serious_offence, OffenseType) - - -class AvailabilityWindow: - """ - A cyclic array with a maximum length that holds boolean values. - - When an element is added to the array and the maximum length has been reached, - the oldest element is removed. Two attributes `num_positive` and `num_negative` - reflect the number of positive and negative elements in the AvailabilityWindow, - they are updated every time a new element is added. - """ - - def __init__(self, max_length: int) -> None: - """ - Initializes the `AvailabilityWindow` instance. - - :param max_length: the maximum length of the cyclic array. - """ - if max_length < 1: - raise ValueError( - f"An `AvailabilityWindow` with a `max_length` {max_length} < 1 is not valid." - ) - - self._max_length = max_length - self._window: Deque[bool] = deque(maxlen=max_length) - self._num_positive = 0 - self._num_negative = 0 - - def __eq__(self, other: Any) -> bool: - """Compare `AvailabilityWindow` objects.""" - if isinstance(other, AvailabilityWindow): - return self.to_dict() == other.to_dict() - return False - - def has_bad_availability_rate(self, threshold: float = 0.95) -> bool: - """Whether the agent on which the window belongs to has a bad availability rate or not.""" - return self._num_positive >= ceil(self._max_length * threshold) - - def _update_counters(self, positive: bool, removal: bool = False) -> None: - """Updates the `num_positive` and `num_negative` counters.""" - update_amount = -1 if removal else 1 - - if positive: - if self._num_positive == 0 and update_amount == -1: # pragma: no cover - return - self._num_positive += update_amount - else: - if self._num_negative == 0 and update_amount == -1: # pragma: no cover - return - self._num_negative += update_amount - - def add(self, value: bool) -> None: - """ - Adds a new boolean value to the cyclic array. - - If the maximum length has been reached, the oldest element is removed. - - :param value: The boolean value to add to the cyclic array. - """ - if len(self._window) == self._max_length and self._max_length > 0: - # we have filled the window, we need to pop the oldest element - # and update the score accordingly - oldest_value = self._window.popleft() - self._update_counters(oldest_value, removal=True) - - self._window.append(value) - self._update_counters(value) - - def to_dict(self) -> Dict[str, int]: - """Returns a dictionary representation of the `AvailabilityWindow` instance.""" - return { - "max_length": self._max_length, - # Please note that the value cannot be represented if the max length of the availability window is > 14_285 - "array": ( - int("".join(str(int(flag)) for flag in self._window), base=2) - if len(self._window) - else 0 - ), - "num_positive": self._num_positive, - "num_negative": self._num_negative, - } - - @staticmethod - def _validate_key( - data: Dict[str, int], key: str, validator: Callable[[int], bool] - ) -> None: - """Validate the given key in the data.""" - value = data.get(key, None) - if value is None: - raise ValueError(f"Missing required key: {key}.") - - if not isinstance(value, int): - raise ValueError(f"{key} must be of type int.") - - if not validator(value): - raise ValueError(f"{key} has invalid value {value}.") - - @staticmethod - def _validate(data: Dict[str, int]) -> None: - """Check if the input can be properly mapped to the class attributes.""" - if not isinstance(data, dict): - raise TypeError(f"Expected dict, got {type(data)}") - - attribute_to_validator = { - "max_length": lambda x: x > 0, - "array": lambda x: 0 <= x < 2 ** data["max_length"], - "num_positive": lambda x: x >= 0, - "num_negative": lambda x: x >= 0, - } - - errors = [] - for attribute, validator in attribute_to_validator.items(): - try: - AvailabilityWindow._validate_key(data, attribute, validator) - except ValueError as e: - errors.append(str(e)) - - if errors: - raise ValueError("Invalid input:\n" + "\n".join(errors)) - - @classmethod - def from_dict(cls, data: Dict[str, int]) -> "AvailabilityWindow": - """Initializes an `AvailabilityWindow` instance from a dictionary.""" - cls._validate(data) - - # convert the serialized array to a binary string - binary_number = bin(data["array"])[2:] - # convert each character in the binary string to a flag - flags = (bool(int(digit)) for digit in binary_number) - - instance = cls(max_length=data["max_length"]) - instance._window.extend(flags) - instance._num_positive = data["num_positive"] - instance._num_negative = data["num_negative"] - return instance - - -@dataclass -class OffenceStatus: - """A class that holds information about offence status for an agent.""" - - validator_downtime: AvailabilityWindow = field( - default_factory=lambda: AvailabilityWindow(NUMBER_OF_BLOCKS_TRACKED) - ) - invalid_payload: AvailabilityWindow = field( - default_factory=lambda: AvailabilityWindow(NUMBER_OF_ROUNDS_TRACKED) - ) - blacklisted: AvailabilityWindow = field( - default_factory=lambda: AvailabilityWindow(NUMBER_OF_ROUNDS_TRACKED) - ) - suspected: AvailabilityWindow = field( - default_factory=lambda: AvailabilityWindow(NUMBER_OF_ROUNDS_TRACKED) - ) - num_unknown_offenses: int = 0 - num_double_signed: int = 0 - num_light_client_attack: int = 0 - custom_offences_amount: int = 0 - - def slash_amount(self, light_unit_amount: int, serious_unit_amount: int) -> int: - """Get the slash amount of the current status.""" - offence_types = [] - - if self.validator_downtime.has_bad_availability_rate(): - offence_types.append(OffenseType.VALIDATOR_DOWNTIME) - if self.invalid_payload.has_bad_availability_rate(): - offence_types.append(OffenseType.INVALID_PAYLOAD) - if self.blacklisted.has_bad_availability_rate(): - offence_types.append(OffenseType.BLACKLISTED) - if self.suspected.has_bad_availability_rate(): - offence_types.append(OffenseType.SUSPECTED) - offence_types.extend([OffenseType.UNKNOWN] * self.num_unknown_offenses) - offence_types.extend([OffenseType.UNKNOWN] * self.num_double_signed) - offence_types.extend([OffenseType.UNKNOWN] * self.num_light_client_attack) - - light_multiplier = 0 - serious_multiplier = 0 - for offence_type in offence_types: - light_multiplier += bool(is_light_offence(offence_type)) - serious_multiplier += bool(is_serious_offence(offence_type)) - - return ( - light_multiplier * light_unit_amount - + serious_multiplier * serious_unit_amount - + self.custom_offences_amount - ) - - -class OffenseStatusEncoder(json.JSONEncoder): - """A custom JSON encoder for the offence status dictionary.""" - - def default(self, o: Any) -> Any: - """The default JSON encoder.""" - if is_dataclass(o): - return asdict(o) - if isinstance(o, AvailabilityWindow): - return o.to_dict() - return super().default(o) - - -class OffenseStatusDecoder(json.JSONDecoder): - """A custom JSON decoder for the offence status dictionary.""" - - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Initialize the custom JSON decoder.""" - super().__init__(object_hook=self.hook, *args, **kwargs) - - @staticmethod - def hook( - data: Dict[str, Any] - ) -> Union[AvailabilityWindow, OffenceStatus, Dict[str, OffenceStatus]]: - """Perform the custom decoding.""" - # if this is an `AvailabilityWindow` - window_attributes = sorted(AvailabilityWindow(1).to_dict().keys()) - if window_attributes == sorted(data.keys()): - return AvailabilityWindow.from_dict(data) - - # if this is an `OffenceStatus` - status_attributes = ( - OffenceStatus.__annotations__.keys() # pylint: disable=no-member - ) - if sorted(status_attributes) == sorted(data.keys()): - return OffenceStatus(**data) - - return data - - -@dataclass(frozen=True, eq=True) -class PendingOffense: - """A dataclass to represent offences that need to be addressed.""" - - accused_agent_address: str - round_count: int - offense_type: OffenseType - last_transition_timestamp: float - time_to_live: float - # only takes effect if the `OffenseType` is of type `CUSTOM`, otherwise it is ignored - custom_amount: int = 0 - - def __post_init__(self) -> None: - """Post initialization for offence type conversion in case it is given as an `int`.""" - if isinstance(self.offense_type, int): - super().__setattr__("offense_type", OffenseType(self.offense_type)) - - -class SlashingNotConfiguredError(Exception): - """Custom exception raised when slashing configuration is requested but is not available.""" - - -DEFAULT_PENDING_OFFENCE_TTL = 2 * 60 * 60 # 1 hour - - -class RoundSequence: # pylint: disable=too-many-instance-attributes - """ - This class represents a sequence of rounds - - It is a generic class that keeps track of the current round - of the consensus period. It receives 'deliver_tx' requests - from the ABCI handlers and forwards them to the current - active round instance, which implements the ABCI app logic. - It also schedules the next round (if any) whenever a round terminates. - """ - - class _BlockConstructionState(Enum): - """ - Phases of an ABCI-based block construction. - - WAITING_FOR_BEGIN_BLOCK: the app is ready to accept - "begin_block" requests from the consensus engine node. - Then, it transitions into the 'WAITING_FOR_DELIVER_TX' phase. - WAITING_FOR_DELIVER_TX: the app is building the block - by accepting "deliver_tx" requests, and waits - until the "end_block" request. - Then, it transitions into the 'WAITING_FOR_COMMIT' phase. - WAITING_FOR_COMMIT: the app finished the construction - of the block, but it is waiting for the "commit" - request from the consensus engine node. - Then, it transitions into the 'WAITING_FOR_BEGIN_BLOCK' phase. - """ - - WAITING_FOR_BEGIN_BLOCK = "waiting_for_begin_block" - WAITING_FOR_DELIVER_TX = "waiting_for_deliver_tx" - WAITING_FOR_COMMIT = "waiting_for_commit" - - def __init__(self, context: SkillContext, abci_app_cls: Type[AbciApp]): - """Initialize the round.""" - self._blockchain = Blockchain() - self._syncing_up = True - self._context = context - self._block_construction_phase = ( - RoundSequence._BlockConstructionState.WAITING_FOR_BEGIN_BLOCK - ) - - self._block_builder = BlockBuilder() - self._abci_app_cls = abci_app_cls - self._abci_app: Optional[AbciApp] = None - self._last_round_transition_timestamp: Optional[datetime.datetime] = None - self._last_round_transition_height = 0 - self._last_round_transition_root_hash = b"" - self._last_round_transition_tm_height: Optional[int] = None - self._tm_height: Optional[int] = None - self._block_stall_deadline: Optional[datetime.datetime] = None - self._terminating_round_called: bool = False - # a mapping of the validators' addresses to their agent addresses - # we create a mapping to avoid calculating the agent address from the validator address every time we need it - # since this is an operation that will be performed every time we want to create an offence - self._validator_to_agent: Dict[str, str] = {} - # a mapping of the agents' addresses to their offence status - self._offence_status: Dict[str, OffenceStatus] = {} - self._slashing_enabled = False - self.pending_offences: Set[PendingOffense] = set() - - def enable_slashing(self) -> None: - """Enable slashing.""" - self._slashing_enabled = True - - @property - def validator_to_agent(self) -> Dict[str, str]: - """Get the mapping of the validators' addresses to their agent addresses.""" - if self._validator_to_agent: - return self._validator_to_agent - raise SlashingNotConfiguredError( - "The mapping of the validators' addresses to their agent addresses has not been set." - ) - - @validator_to_agent.setter - def validator_to_agent(self, validator_to_agent: Dict[str, str]) -> None: - """Set the mapping of the validators' addresses to their agent addresses.""" - if self._validator_to_agent: - raise ValueError( - "The mapping of the validators' addresses to their agent addresses can only be set once. " - f"Attempted to set with {validator_to_agent} but it has content already: {self._validator_to_agent}." - ) - self._validator_to_agent = validator_to_agent - - @property - def offence_status(self) -> Dict[str, OffenceStatus]: - """Get the mapping of the agents' addresses to their offence status.""" - if self._offence_status: - return self._offence_status - raise SlashingNotConfiguredError( # pragma: nocover - "The mapping of the agents' addresses to their offence status has not been set." - ) - - @offence_status.setter - def offence_status(self, offence_status: Dict[str, OffenceStatus]) -> None: - """Set the mapping of the agents' addresses to their offence status.""" - self.abci_app.logger.debug(f"Setting offence status to: {offence_status}") - self._offence_status = offence_status - self.store_offence_status() - - def add_pending_offence(self, pending_offence: PendingOffense) -> None: - """ - Add a pending offence to the set of pending offences. - - Pending offences are offences that have been detected, but not yet agreed upon by the consensus. - A pending offence is removed from the set of pending offences and added to the OffenceStatus of a validator - when the majority of the agents agree on it. - - :param pending_offence: the pending offence to add - :return: None - """ - self.pending_offences.add(pending_offence) - - def sync_db_and_slashing(self, serialized_db_state: str) -> None: - """Sync the database and the slashing configuration.""" - self.abci_app.synchronized_data.db.sync(serialized_db_state) - offence_status = self.latest_synchronized_data.slashing_config - if offence_status: - # deserialize the offence status and load it to memory - self.offence_status = json.loads( - offence_status, - cls=OffenseStatusDecoder, - ) - - def serialized_offence_status(self) -> str: - """Serialize the offence status.""" - return json.dumps(self.offence_status, cls=OffenseStatusEncoder, sort_keys=True) - - def store_offence_status(self) -> None: - """Store the serialized offence status.""" - if not self._slashing_enabled: - # if slashing is not enabled, we do not update anything - return - encoded_status = self.serialized_offence_status() - self.latest_synchronized_data.slashing_config = encoded_status - self.abci_app.logger.debug(f"Updated db with: {encoded_status}") - self.abci_app.logger.debug(f"App hash now is: {self.root_hash.hex()}") - - def get_agent_address(self, validator: Validator) -> str: - """Get corresponding agent address from a `Validator` instance.""" - validator_address = validator.address.hex().upper() - - try: - return self.validator_to_agent[validator_address] - except KeyError as exc: - raise ValueError( - f"Requested agent address for an unknown validator address {validator_address}. " - f"Available validators are: {self.validator_to_agent.keys()}" - ) from exc - - def setup(self, *args: Any, **kwargs: Any) -> None: - """ - Set up the round sequence. - - :param args: the arguments to pass to the round constructor. - :param kwargs: the keyword-arguments to pass to the round constructor. - """ - kwargs["context"] = self._context - self._abci_app = self._abci_app_cls(*args, **kwargs) - self._abci_app.setup() - - def start_sync( - self, - ) -> None: # pragma: nocover - """ - Set `_syncing_up` flag to true. - - if the _syncing_up flag is set to true, the `async_act` method won't be executed. For more details refer to - https://github.com/valory-xyz/open-autonomy/issues/247#issuecomment-1012268656 - """ - self._syncing_up = True - - def end_sync( - self, - ) -> None: - """Set `_syncing_up` flag to false.""" - self._syncing_up = False - - @property - def syncing_up( - self, - ) -> bool: - """Return if the app is in sync mode.""" - return self._syncing_up - - @property - def abci_app(self) -> AbciApp: - """Get the AbciApp.""" - if self._abci_app is None: - raise ABCIAppInternalError("AbciApp not set") # pragma: nocover - return self._abci_app - - @property - def blockchain(self) -> Blockchain: - """Get the Blockchain instance.""" - return self._blockchain - - @blockchain.setter - def blockchain(self, _blockchain: Blockchain) -> None: - """Get the Blockchain instance.""" - self._blockchain = _blockchain - - @property - def height(self) -> int: - """Get the height.""" - return self._blockchain.height - - @property - def is_finished(self) -> bool: - """Check if a round sequence has finished.""" - return self.abci_app.is_finished - - def check_is_finished(self) -> None: - """Check if a round sequence has finished.""" - if self.is_finished: - raise ValueError( - "round sequence is finished, cannot accept new transactions" - ) - - @property - def current_round(self) -> AbstractRound: - """Get current round.""" - return self.abci_app.current_round - - @property - def current_round_id(self) -> Optional[str]: - """Get the current round id.""" - return self.abci_app.current_round_id - - @property - def current_round_height(self) -> int: - """Get the current round height.""" - return self.abci_app.current_round_height - - @property - def last_round_id(self) -> Optional[str]: - """Get the last round id.""" - return self.abci_app.last_round_id - - @property - def last_timestamp(self) -> datetime.datetime: - """Get the last timestamp.""" - last_timestamp = ( - self._blockchain.blocks[-1].timestamp - if self._blockchain.length != 0 - else None - ) - if last_timestamp is None: - raise ABCIAppInternalError("last timestamp is None") - return last_timestamp - - @property - def last_round_transition_timestamp( - self, - ) -> datetime.datetime: - """Returns the timestamp for last round transition.""" - if self._last_round_transition_timestamp is None: - raise ValueError( - "Trying to access `last_round_transition_timestamp` while no transition has been completed yet." - ) - - return self._last_round_transition_timestamp - - @property - def last_round_transition_height( - self, - ) -> int: - """Returns the height for last round transition.""" - if self._last_round_transition_height == 0: - raise ValueError( - "Trying to access `last_round_transition_height` while no transition has been completed yet." - ) - - return self._last_round_transition_height - - @property - def last_round_transition_root_hash( - self, - ) -> bytes: - """Returns the root hash for last round transition.""" - if self._last_round_transition_root_hash == b"": - # if called for the first chain initialization, return the hash resulting from the initial abci app's state - return self.root_hash - return self._last_round_transition_root_hash - - @property - def last_round_transition_tm_height(self) -> int: - """Returns the Tendermint height for last round transition.""" - if self._last_round_transition_tm_height is None: - raise ValueError( - "Trying to access Tendermint's last round transition height before any `end_block` calls." - ) - return self._last_round_transition_tm_height - - @property - def latest_synchronized_data(self) -> BaseSynchronizedData: - """Get the latest synchronized_data.""" - return self.abci_app.synchronized_data - - @property - def root_hash(self) -> bytes: - """ - Get the Merkle root hash of the application state. - - This is going to be the database's hash. - In this way, the app hash will be reflecting our application's state, - and will guarantee that all the agents on the chain apply the changes of the arriving blocks in the same way. - - :return: the root hash to be included as the Header.AppHash in the next block. - """ - return self.abci_app.synchronized_data.db.hash() - - @property - def tm_height(self) -> int: - """Get Tendermint's current height.""" - if self._tm_height is None: - raise ValueError( - "Trying to access Tendermint's current height before any `end_block` calls." - ) - return self._tm_height - - @tm_height.setter - def tm_height(self, _tm_height: int) -> None: - """Set Tendermint's current height.""" - self._tm_height = _tm_height - - @property - def block_stall_deadline_expired(self) -> bool: - """Get if the deadline for not having received any begin block requests from the Tendermint node has expired.""" - if self._block_stall_deadline is None: - return False - return datetime.datetime.now() > self._block_stall_deadline - - def set_block_stall_deadline(self) -> None: - """Use the local time of the agent and a predefined tolerance, to specify the expiration of the deadline.""" - self._block_stall_deadline = datetime.datetime.now() + datetime.timedelta( - seconds=BLOCKS_STALL_TOLERANCE - ) - - def init_chain(self, initial_height: int) -> None: - """Init chain.""" - # reduce `initial_height` by 1 to get block count offset as per Tendermint protocol - self._blockchain = Blockchain(initial_height - 1) - - def _track_tm_offences( - self, evidences: Evidences, last_commit_info: LastCommitInfo - ) -> None: - """Track offences provided by Tendermint, if there are any.""" - for vote_info in last_commit_info.votes: - agent_address = self.get_agent_address(vote_info.validator) - was_down = not vote_info.signed_last_block - self.offence_status[agent_address].validator_downtime.add(was_down) - - for byzantine_validator in evidences.byzantine_validators: - agent_address = self.get_agent_address(byzantine_validator.validator) - evidence_type = byzantine_validator.evidence_type - self.offence_status[agent_address].num_unknown_offenses += bool( - evidence_type == EvidenceType.UNKNOWN - ) - self.offence_status[agent_address].num_double_signed += bool( - evidence_type == EvidenceType.DUPLICATE_VOTE - ) - self.offence_status[agent_address].num_light_client_attack += bool( - evidence_type == EvidenceType.LIGHT_CLIENT_ATTACK - ) - - def _track_app_offences(self) -> None: - """Track offences provided by the app level, if there are any.""" - synced_data = self.abci_app.synchronized_data - for agent in self.offence_status.keys(): - blacklisted = agent in synced_data.blacklisted_keepers - suspected = agent in cast(tuple, synced_data.db.get("suspects", tuple())) - agent_status = self.offence_status[agent] - agent_status.blacklisted.add(blacklisted) - agent_status.suspected.add(suspected) - - def _handle_slashing_not_configured(self, exc: SlashingNotConfiguredError) -> None: - """Handle a `SlashingNotConfiguredError`.""" - # In the current slashing implementation, we do not track offences before setting the slashing - # configuration, i.e., before successfully sharing the tm configuration via ACN on registration. - # That is because we cannot slash an agent if we do not map their validator address to their agent address. - # Checking the number of participants will allow us to identify whether the registration round has finished, - # and therefore expect that the slashing configuration has been set if ACN registration is enabled. - if self.abci_app.synchronized_data.nb_participants: - _logger.error( - f"{exc} This error may occur when the ACN registration has not been successfully performed. " - "Have you set the `share_tm_config_on_startup` flag to `true` in the configuration?" - ) - self._slashing_enabled = False - _logger.warning("Slashing has been disabled!") - - def _try_track_offences( - self, evidences: Evidences, last_commit_info: LastCommitInfo - ) -> None: - """Try to track the offences. If an error occurs, log it, disable slashing, and warn about the latter.""" - try: - if self._slashing_enabled: - # only track offences if the first round has finished - # we avoid tracking offences in the first round - # because we do not have the slashing configuration synced yet - self._track_tm_offences(evidences, last_commit_info) - self._track_app_offences() - except SlashingNotConfiguredError as exc: - self._handle_slashing_not_configured(exc) - - def begin_block( - self, - header: Header, - evidences: Evidences, - last_commit_info: LastCommitInfo, - ) -> None: - """Begin block.""" - if self.is_finished: - raise ABCIAppInternalError( - "round sequence is finished, cannot accept new blocks" - ) - if ( - self._block_construction_phase - != RoundSequence._BlockConstructionState.WAITING_FOR_BEGIN_BLOCK - ): - raise ABCIAppInternalError( - f"cannot accept a 'begin_block' request. Current phase={self._block_construction_phase}" - ) - - # From now on, the ABCI app waits for 'deliver_tx' requests, until 'end_block' is received - self._block_construction_phase = ( - RoundSequence._BlockConstructionState.WAITING_FOR_DELIVER_TX - ) - self._block_builder.reset() - self._block_builder.header = header - self.abci_app.update_time(header.timestamp) - self.set_block_stall_deadline() - self.abci_app.logger.debug( - "Created a new local deadline for the next `begin_block` request from the Tendermint node: " - f"{self._block_stall_deadline}" - ) - self._try_track_offences(evidences, last_commit_info) - - def deliver_tx(self, transaction: Transaction) -> None: - """ - Deliver a transaction. - - Appends the transaction to build the block on 'end_block' later. - :param transaction: the transaction. - :raises: an Error otherwise. - """ - if ( - self._block_construction_phase - != RoundSequence._BlockConstructionState.WAITING_FOR_DELIVER_TX - ): - raise ABCIAppInternalError( - f"cannot accept a 'deliver_tx' request. Current phase={self._block_construction_phase}" - ) - - self.abci_app.check_transaction(transaction) - self.abci_app.process_transaction(transaction) - self._block_builder.add_transaction(transaction) - - def end_block(self) -> None: - """Process the 'end_block' request.""" - if ( - self._block_construction_phase - != RoundSequence._BlockConstructionState.WAITING_FOR_DELIVER_TX - ): - raise ABCIAppInternalError( - f"cannot accept a 'end_block' request. Current phase={self._block_construction_phase}" - ) - # The ABCI app waits for the commit - self._block_construction_phase = ( - RoundSequence._BlockConstructionState.WAITING_FOR_COMMIT - ) - - def commit(self) -> None: - """Process the 'commit' request.""" - if ( - self._block_construction_phase - != RoundSequence._BlockConstructionState.WAITING_FOR_COMMIT - ): - raise ABCIAppInternalError( - f"cannot accept a 'commit' request. Current phase={self._block_construction_phase}" - ) - block = self._block_builder.get_block() - try: - if self._blockchain.is_init: - # There are occasions where we wait for an init_chain() before accepting blocks. - # This can happen during hard reset, where we might've reset the local blockchain, - # But are still receiving requests from the not yet reset tendermint node. - # We only process blocks on an initialized local blockchain. - # The local blockchain gets initialized upon receiving an init_chain request from - # the tendermint node. In cases where we don't want to wait for the init_chain req, - # one can create a Blockchain instance with `is_init=True`, i.e. the default args. - self._blockchain.add_block(block) - self._update_round() - else: - self.abci_app.logger.warning( - f"Received block with height {block.header.height} before the blockchain was initialized." - ) - # The ABCI app now waits again for the next block - self._block_construction_phase = ( - RoundSequence._BlockConstructionState.WAITING_FOR_BEGIN_BLOCK - ) - except AddBlockError as exception: - raise exception - - def reset_blockchain(self, is_replay: bool = False, is_init: bool = False) -> None: - """ - Reset blockchain after tendermint reset. - - :param is_replay: whether we are resetting the blockchain while replaying blocks. - :param is_init: whether to process blocks before receiving an init_chain req from tendermint. - """ - if is_replay: - self._block_construction_phase = ( - RoundSequence._BlockConstructionState.WAITING_FOR_BEGIN_BLOCK - ) - self._blockchain = Blockchain(is_init=is_init) - - def _get_round_result( - self, - ) -> Optional[Tuple[BaseSynchronizedData, Any]]: - """ - Get the round's result. - - Give priority to: - 1. terminating bg rounds - 2. ever running bg rounds - 3. normal bg rounds - 4. normal rounds - - :return: the round's result. - """ - for prioritized_group in self.abci_app.bg_apps_prioritized: - for app in prioritized_group: - result = app.background_round.end_block() - if ( - result is None - or app.type == BackgroundAppType.TERMINATING - and self._terminating_round_called - ): - continue - if ( - app.type == BackgroundAppType.TERMINATING - and not self._terminating_round_called - ): - self._terminating_round_called = True - return result - return self.abci_app.current_round.end_block() - - def _update_round(self) -> None: - """ - Update a round. - - Check whether the round has finished. If so, get the new round and set it as the current round. - If a termination app's round has returned a result, then the other apps' rounds are ignored. - """ - result = self._get_round_result() - - if result is None: - # neither the background rounds, nor the current round returned, so no update needs to be made - return - - # update the offence status at the end of each round - # this is done to ensure that the offence status is always up-to-date & in sync - # the next step is a no-op if slashing is not enabled - self.store_offence_status() - - self._last_round_transition_timestamp = self._blockchain.last_block.timestamp - self._last_round_transition_height = self.height - self._last_round_transition_root_hash = self.root_hash - self._last_round_transition_tm_height = self.tm_height - - round_result, event = result - self.abci_app.logger.debug( - f"updating round, current_round {self.current_round.round_id}, event: {event}, round result {round_result}" - ) - self.abci_app.process_event(event, result=round_result) - - def _reset_to_default_params(self) -> None: - """Resets the instance params to their default value.""" - self._last_round_transition_timestamp = None - self._last_round_transition_height = 0 - self._last_round_transition_root_hash = b"" - self._last_round_transition_tm_height = None - self._tm_height = None - self._slashing_enabled = False - self.pending_offences = set() - - def reset_state( - self, - restart_from_round: str, - round_count: int, - serialized_db_state: Optional[str] = None, - ) -> None: - """ - This method resets the state of RoundSequence to the beginning of the period. - - Note: This is intended to be used for agent <-> tendermint communication recovery only! - - :param restart_from_round: from which round to restart the abci. - This round should be the first round in the last period. - :param round_count: the round count at the beginning of the period -1. - :param serialized_db_state: the state of the database at the beginning of the period. - If provided, the database will be reset to this state. - """ - self._reset_to_default_params() - self.abci_app.synchronized_data.db.round_count = round_count - if serialized_db_state is not None: - self.sync_db_and_slashing(serialized_db_state) - # Furthermore, that hash is then in turn used as the init hash when the tm network is reset. - self._last_round_transition_root_hash = self.root_hash - - self.abci_app.cleanup_timeouts() - round_id_to_cls = { - cls.auto_round_id(): cls for cls in self.abci_app.transition_function - } - restart_from_round_cls = round_id_to_cls.get(restart_from_round, None) - if restart_from_round_cls is None: - raise ABCIAppInternalError( - "Cannot reset state. The Tendermint recovery parameters are incorrect. " - "Did you update the `restart_from_round` with an incorrect round id? " - f"Found {restart_from_round}, but the app's transition function has the following round ids: " - f"{set(round_id_to_cls.keys())}." - ) - self.abci_app.schedule_round(restart_from_round_cls) - - -@dataclass(frozen=True) -class PendingOffencesPayload(BaseTxPayload): - """Represent a transaction payload for pending offences.""" - - accused_agent_address: str - offense_round: int - offense_type_value: int - last_transition_timestamp: float - time_to_live: float - custom_amount: int - - -class PendingOffencesRound(CollectSameUntilThresholdRound): - """Defines the pending offences background round, which runs concurrently with other rounds to sync the offences.""" - - payload_class = PendingOffencesPayload - synchronized_data_class = BaseSynchronizedData - - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Initialize the `PendingOffencesRound`.""" - super().__init__(*args, **kwargs) - self._latest_round_processed = -1 - - @property - def offence_status(self) -> Dict[str, OffenceStatus]: - """Get the offence status from the round sequence.""" - return self.context.state.round_sequence.offence_status - - def end_block(self) -> None: - """ - Process the end of the block for the pending offences background round. - - It is important to note that this is a non-standard type of round, meaning it does not emit any events. - Instead, it continuously runs in the background. - The objective of this round is to consistently monitor the received pending offences - and achieve a consensus among the agents. - """ - if not self.threshold_reached: - return - - offence = PendingOffense(*self.most_voted_payload_values) - - # an offence should only be tracked once, not every time a payload is processed after the threshold is reached - if self._latest_round_processed == offence.round_count: - return - - # add synchronized offence to the offence status - # only `INVALID_PAYLOAD` offence types are supported at the moment as pending offences: - # https://github.com/valory-xyz/open-autonomy/blob/6831d6ebaf10ea8e3e04624b694c7f59a6d05bb4/packages/valory/skills/abstract_round_abci/handlers.py#L215-L222 # noqa - invalid = offence.offense_type == OffenseType.INVALID_PAYLOAD - self.offence_status[offence.accused_agent_address].invalid_payload.add(invalid) - - # if the offence is of custom type, then add the custom amount to it - if offence.offense_type == OffenseType.CUSTOM: - self.offence_status[ - offence.accused_agent_address - ].custom_offences_amount += offence.custom_amount - elif offence.custom_amount != 0: - self.context.logger.warning( - f"Custom amount for {offence=} will not take effect as it is not of `CUSTOM` type." - ) - - self._latest_round_processed = offence.round_count diff --git a/packages/valory/skills/abstract_round_abci/behaviour_utils.py b/packages/valory/skills/abstract_round_abci/behaviour_utils.py deleted file mode 100644 index 43f9298..0000000 --- a/packages/valory/skills/abstract_round_abci/behaviour_utils.py +++ /dev/null @@ -1,2356 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains helper classes for behaviours.""" - - -import datetime -import inspect -import json -import pprint -import re -import sys -from abc import ABC, ABCMeta, abstractmethod -from enum import Enum -from functools import partial -from typing import ( - Any, - Callable, - Dict, - Generator, - List, - Optional, - Tuple, - Type, - Union, - cast, -) - -import pytz -from aea.exceptions import enforce -from aea.mail.base import EnvelopeContext -from aea.protocols.base import Message -from aea.protocols.dialogue.base import Dialogue -from aea.skills.behaviours import SimpleBehaviour - -from packages.open_aea.protocols.signing import SigningMessage -from packages.open_aea.protocols.signing.custom_types import ( - RawMessage, - RawTransaction, - SignedTransaction, - Terms, -) -from packages.valory.connections.http_client.connection import ( - PUBLIC_ID as HTTP_CLIENT_PUBLIC_ID, -) -from packages.valory.connections.ipfs.connection import PUBLIC_ID as IPFS_CONNECTION_ID -from packages.valory.connections.p2p_libp2p_client.connection import ( - PUBLIC_ID as P2P_LIBP2P_CLIENT_PUBLIC_ID, -) -from packages.valory.contracts.service_registry.contract import ( # noqa: F401 # pylint: disable=unused-import - ServiceRegistryContract, -) -from packages.valory.protocols.contract_api import ContractApiMessage -from packages.valory.protocols.http import HttpMessage -from packages.valory.protocols.ipfs import IpfsMessage -from packages.valory.protocols.ipfs.dialogues import IpfsDialogue, IpfsDialogues -from packages.valory.protocols.ledger_api import LedgerApiMessage -from packages.valory.protocols.tendermint import TendermintMessage -from packages.valory.skills.abstract_round_abci.base import ( - AbstractRound, - BaseSynchronizedData, - BaseTxPayload, - LEDGER_API_ADDRESS, - OK_CODE, - RoundSequence, - Transaction, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - ContractApiDialogue, - ContractApiDialogues, - HttpDialogue, - HttpDialogues, - LedgerApiDialogue, - LedgerApiDialogues, - SigningDialogues, - TendermintDialogues, -) -from packages.valory.skills.abstract_round_abci.io_.ipfs import ( - IPFSInteract, - IPFSInteractionError, -) -from packages.valory.skills.abstract_round_abci.io_.load import CustomLoaderType, Loader -from packages.valory.skills.abstract_round_abci.io_.store import ( - CustomStorerType, - Storer, - SupportedFiletype, - SupportedObjectType, -) -from packages.valory.skills.abstract_round_abci.models import ( - BaseParams, - Requests, - SharedState, - TendermintRecoveryParams, -) - - -# TODO: port registration code from registration_abci to here - - -NON_200_RETURN_CODE_DURING_RESET_THRESHOLD = 3 -GENESIS_TIME_FMT = "%Y-%m-%dT%H:%M:%S.%fZ" -INITIAL_APP_HASH = "" -INITIAL_HEIGHT = "0" -TM_REQ_TIMEOUT = 5 # 5 seconds -FLASHBOTS_LEDGER_ID = "ethereum_flashbots" -SOLANA_LEDGER_ID = "solana" - - -class SendException(Exception): - """Exception raised if the 'try_send' to an AsyncBehaviour failed.""" - - -class TimeoutException(Exception): - """Exception raised when a timeout during AsyncBehaviour occurs.""" - - -class BaseBehaviourInternalError(Exception): - """Internal error due to a bad implementation of the BaseBehaviour.""" - - def __init__(self, message: str, *args: Any) -> None: - """Initialize the error object.""" - super().__init__("internal error: " + message, *args) - - -class AsyncBehaviour(ABC): - """ - MixIn behaviour class that support limited asynchronous programming. - - An AsyncBehaviour can be in three states: - - READY: no suspended 'async_act' execution; - - RUNNING: 'act' called, and waiting for a message - - WAITING_TICK: 'act' called, and waiting for the next 'act' call - """ - - class AsyncState(Enum): - """Enumeration of AsyncBehaviour states.""" - - READY = "ready" - RUNNING = "running" - WAITING_MESSAGE = "waiting_message" - - def __init__(self) -> None: - """Initialize the async behaviour.""" - self.__state = self.AsyncState.READY - self.__generator_act: Optional[Generator] = None - - # temporary variables for the waiting message state - self.__stopped: bool = True - self.__notified: bool = False - self.__message: Any = None - self.__setup_called: bool = False - - @abstractmethod - def async_act(self) -> Generator: - """Do the act, supporting asynchronous execution.""" - - @abstractmethod - def async_act_wrapper(self) -> Generator: - """Do the act, supporting asynchronous execution.""" - - @property - def state(self) -> AsyncState: - """Get the 'async state'.""" - return self.__state - - @property - def is_notified(self) -> bool: - """Returns whether the behaviour has been notified about the arrival of a message.""" - return self.__notified - - @property - def received_message(self) -> Any: - """Returns the message the behaviour has received. "__message" should be None if not availble or already consumed.""" - return self.__message - - def _on_sent_message(self) -> None: - """To be called after the message received is consumed. Removes the already sent notification and message.""" - self.__notified = False - self.__message = None - - @property - def is_stopped(self) -> bool: - """Check whether the behaviour has stopped.""" - return self.__stopped - - def __get_generator_act(self) -> Generator: - """Get the _generator_act.""" - if self.__generator_act is None: - raise ValueError("generator act not set!") # pragma: nocover - return self.__generator_act - - def try_send(self, message: Any) -> None: - """ - Try to send a message to a waiting behaviour. - - It will be sent only if the behaviour is actually waiting for a message, - and it was not already notified. - - :param message: a Python object. - :raises: SendException if the behaviour was not waiting for a message, - or if it was already notified. - """ - in_waiting_message_state = self.__state == self.AsyncState.WAITING_MESSAGE - already_notified = self.__notified - enforce( - in_waiting_message_state and not already_notified, - "cannot send message", - exception_class=SendException, - ) - self.__notified = True - self.__message = message - - @classmethod - def wait_for_condition( - cls, condition: Callable[[], bool], timeout: Optional[float] = None - ) -> Generator[None, None, None]: - """Wait for a condition to happen. - - This is a local method that does not depend on the global clock, - so the usage of datetime.now() is acceptable here. - - :param condition: the condition to wait for - :param timeout: the maximum amount of time to wait - :yield: None - """ - if timeout is not None: - deadline = datetime.datetime.now() + datetime.timedelta(0, timeout) - else: - deadline = datetime.datetime.max - - while not condition(): - if timeout is not None and datetime.datetime.now() > deadline: - raise TimeoutException() - yield - - def sleep(self, seconds: float) -> Any: - """ - Delay execution for a given number of seconds. - - The argument may be a floating point number for subsecond precision. - This is a local method that does not depend on the global clock, so the - usage of datetime.now() is acceptable here. - - :param seconds: the seconds - :yield: None - """ - deadline = datetime.datetime.now() + datetime.timedelta(0, seconds) - - def _wait_until() -> bool: - return datetime.datetime.now() > deadline - - yield from self.wait_for_condition(_wait_until) - - def wait_for_message( - self, - condition: Callable = lambda message: True, - timeout: Optional[float] = None, - ) -> Any: - """ - Wait for message. - - Care must be taken. This method does not handle concurrent requests. - Use directly after a request is being sent. - This is a local method that does not depend on the global clock, - so the usage of datetime.now() is acceptable here. - - :param condition: a callable - :param timeout: max time to wait (in seconds) - :return: a message - :yield: None - """ - if timeout is not None: - deadline = datetime.datetime.now() + datetime.timedelta(0, timeout) - else: - deadline = datetime.datetime.max - - self.__state = self.AsyncState.WAITING_MESSAGE - try: - message = None - while message is None or not condition(message): - message = yield - if timeout is not None and datetime.datetime.now() > deadline: - raise TimeoutException() - message = cast(Message, message) - return message - finally: - self.__state = self.AsyncState.RUNNING - - def setup(self) -> None: # noqa: B027 # flake8 suggest make it abstract - """Setup behaviour.""" - - def act(self) -> None: - """Do the act.""" - # call setup only the first time act is called - if not self.__setup_called: - self.setup() - self.__setup_called = True - - if self.__state == self.AsyncState.READY: - self.__call_act_first_time() - return - if self.__state == self.AsyncState.WAITING_MESSAGE: - self.__handle_waiting_for_message() - return - enforce(self.__state == self.AsyncState.RUNNING, "not in 'RUNNING' state") - self.__handle_tick() - - def stop(self) -> None: - """Stop the execution of the behaviour.""" - if self.__stopped or self.__state == self.AsyncState.READY: - return - self.__get_generator_act().close() - self.__state = self.AsyncState.READY - self.__stopped = True - - def __call_act_first_time(self) -> None: - """Call the 'async_act' method for the first time.""" - self.__stopped = False - self.__state = self.AsyncState.RUNNING - try: - self.__generator_act = self.async_act_wrapper() - # if the method 'async_act' was not a generator function - # (i.e. no 'yield' or 'yield from' statement) - # just return - if not inspect.isgenerator(self.__generator_act): - self.__state = self.AsyncState.READY - return - # trigger first execution, up to next 'yield' statement - self.__get_generator_act().send(None) - except StopIteration: - # this may happen if the generator is empty - self.__state = self.AsyncState.READY - - def __handle_waiting_for_message(self) -> None: - """Handle an 'act' tick, when waiting for a message.""" - # if there is no message coming, skip. - if self.__notified: - try: - self.__get_generator_act().send(self.__message) - except StopIteration: - self.__handle_stop_iteration() - finally: - # wait for the next message - self.__notified = False - self.__message = None - - def __handle_tick(self) -> None: - """Handle an 'act' tick.""" - try: - self.__get_generator_act().send(None) - except StopIteration: - self.__handle_stop_iteration() - - def __handle_stop_iteration(self) -> None: - """ - Handle 'StopIteration' exception. - - The exception means that the 'async_act' - generator function terminated the execution, - and therefore the state needs to be reset. - """ - self.__state = self.AsyncState.READY - - -class IPFSBehaviour(SimpleBehaviour, ABC): - """Behaviour for interactions with IPFS.""" - - def __init__(self, **kwargs: Any): - """Initialize an `IPFSBehaviour`.""" - super().__init__(**kwargs) - loader_cls = kwargs.pop("loader_cls", Loader) - storer_cls = kwargs.pop("storer_cls", Storer) - self._ipfs_interact = IPFSInteract(loader_cls, storer_cls) - - def _build_ipfs_message( - self, - performative: IpfsMessage.Performative, - timeout: Optional[float] = None, - **kwargs: Any, - ) -> Tuple[IpfsMessage, IpfsDialogue]: - """Builds an IPFS message.""" - ipfs_dialogues = cast(IpfsDialogues, self.context.ipfs_dialogues) - message, dialogue = ipfs_dialogues.create( - counterparty=str(IPFS_CONNECTION_ID), - performative=performative, - timeout=timeout, - **kwargs, - ) - return message, dialogue - - def _build_ipfs_store_file_req( # pylint: disable=too-many-arguments - self, - filename: str, - obj: SupportedObjectType, - multiple: bool = False, - filetype: Optional[SupportedFiletype] = None, - custom_storer: Optional[CustomStorerType] = None, - timeout: Optional[float] = None, - **kwargs: Any, - ) -> Tuple[IpfsMessage, IpfsDialogue]: - """ - Builds a STORE_FILES ipfs message. - - :param filename: the file name to store obj in. If "multiple" is True, filename will be the name of the dir. - :param obj: the object(s) to serialize and store in IPFS as "filename". - :param multiple: whether obj should be stored as multiple files, i.e. directory. - :param custom_storer: a custom serializer for "obj". - :param timeout: timeout for the request. - :returns: the ipfs message, and its corresponding dialogue. - """ - serialized_objects = self._ipfs_interact.store( - filename, obj, multiple, filetype, custom_storer, **kwargs - ) - message, dialogue = self._build_ipfs_message( - performative=IpfsMessage.Performative.STORE_FILES, # type: ignore - files=serialized_objects, - timeout=timeout, - ) - return message, dialogue - - def _build_ipfs_get_file_req( - self, - ipfs_hash: str, - timeout: Optional[float] = None, - ) -> Tuple[IpfsMessage, IpfsDialogue]: - """ - Builds a GET_FILES IPFS request. - - :param ipfs_hash: the ipfs hash of the file/dir to download. - :param timeout: timeout for the request. - :returns: the ipfs message, and its corresponding dialogue. - """ - message, dialogue = self._build_ipfs_message( - performative=IpfsMessage.Performative.GET_FILES, # type: ignore - ipfs_hash=ipfs_hash, - timeout=timeout, - ) - return message, dialogue - - def _deserialize_ipfs_objects( # pylint: disable=too-many-arguments - self, - serialized_objects: Dict[str, str], - filetype: Optional[SupportedFiletype] = None, - custom_loader: CustomLoaderType = None, - ) -> Optional[SupportedObjectType]: - """Deserialize objects received from IPFS.""" - deserialized_object = self._ipfs_interact.load( - serialized_objects, filetype, custom_loader - ) - return deserialized_object - - -class CleanUpBehaviour(SimpleBehaviour, ABC): - """Class for clean-up related functionality of behaviours.""" - - def __init__(self, **kwargs: Any): # pylint: disable=super-init-not-called - """Initialize a base behaviour.""" - SimpleBehaviour.__init__(self, **kwargs) - - def clean_up(self) -> None: - """ - Clean up the resources due to a 'stop' event. - - It can be optionally implemented by the concrete classes. - """ - - def handle_late_messages(self, behaviour_id: str, message: Message) -> None: - """ - Handle late arriving messages. - - Runs from another behaviour, even if the behaviour implementing the method has been exited. - It can be optionally implemented by the concrete classes. - - :param behaviour_id: the id of the behaviour in which the message belongs to. - :param message: the late arriving message to handle. - """ - request_nonce = message.dialogue_reference[0] - self.context.logger.warning( - f"No callback defined for request with nonce: {request_nonce}, arriving for behaviour: {behaviour_id}" - ) - - -class RPCResponseStatus(Enum): - """A custom status of an RPC response.""" - - SUCCESS = 1 - INCORRECT_NONCE = 2 - UNDERPRICED = 3 - INSUFFICIENT_FUNDS = 4 - ALREADY_KNOWN = 5 - UNCLASSIFIED_ERROR = 6 - SIMULATION_FAILED = 7 - - -class _MetaBaseBehaviour(ABCMeta): - """A metaclass that validates BaseBehaviour's attributes.""" - - def __new__(mcs, name: str, bases: Tuple, namespace: Dict, **kwargs: Any) -> Type: # type: ignore - """Initialize the class.""" - new_cls = super().__new__(mcs, name, bases, namespace, **kwargs) - - if ABC in bases: - # abstract class, return - return new_cls - if not issubclass(new_cls, BaseBehaviour): - # the check only applies to AbciApp subclasses - return new_cls - - mcs._check_consistency(cast(Type[BaseBehaviour], new_cls)) - return new_cls - - @classmethod - def _check_consistency(mcs, base_behaviour_cls: Type["BaseBehaviour"]) -> None: - """Check consistency of class attributes.""" - mcs._check_required_class_attributes(base_behaviour_cls) - - @classmethod - def _check_required_class_attributes( - mcs, base_behaviour_cls: Type["BaseBehaviour"] - ) -> None: - """Check that required class attributes are set.""" - if not hasattr(base_behaviour_cls, "matching_round"): - raise BaseBehaviourInternalError( - f"'matching_round' not set on {base_behaviour_cls}" - ) - - -class BaseBehaviour( - AsyncBehaviour, IPFSBehaviour, CleanUpBehaviour, ABC, metaclass=_MetaBaseBehaviour -): - """ - This class represents the base class for FSM behaviours - - A behaviour is a state of the FSM App execution. It usually involves - interactions between participants in the FSM App, - although this is not enforced at this level of abstraction. - - Concrete classes must set: - - matching_round: the round class matching the behaviour; - - Optionally, behaviour_id can be defined, although it is recommended to use the autogenerated id. - """ - - __pattern = re.compile(r"(? str: - """ - Get behaviour id automatically. - - This method returns the auto generated id from the class name if the - class variable behaviour_id is not set on the child class. - Otherwise, it returns the class variable behaviour_id. - """ - return ( - cls.behaviour_id - if isinstance(cls.behaviour_id, str) - else cls.__pattern.sub("_", cls.__name__).lower() - ) - - @property # type: ignore - def behaviour_id(self) -> str: - """Get behaviour id.""" - return self.auto_behaviour_id() - - @property - def params(self) -> BaseParams: - """Return the params.""" - return self.context.params - - @property - def shared_state(self) -> SharedState: - """Return the round sequence.""" - return self.context.state - - @property - def round_sequence(self) -> RoundSequence: - """Return the round sequence.""" - return self.shared_state.round_sequence - - @property - def synchronized_data(self) -> BaseSynchronizedData: - """Return the synchronized data.""" - return self.shared_state.synchronized_data - - @property - def tm_communication_unhealthy(self) -> bool: - """Return if the Tendermint communication is not healthy anymore.""" - return self.round_sequence.block_stall_deadline_expired - - def check_in_round(self, round_id: str) -> bool: - """Check that we entered a specific round.""" - return self.round_sequence.current_round_id == round_id - - def check_in_last_round(self, round_id: str) -> bool: - """Check that we entered a specific round.""" - return self.round_sequence.last_round_id == round_id - - def check_not_in_round(self, round_id: str) -> bool: - """Check that we are not in a specific round.""" - return not self.check_in_round(round_id) - - def check_not_in_last_round(self, round_id: str) -> bool: - """Check that we are not in a specific round.""" - return not self.check_in_last_round(round_id) - - def check_round_has_finished(self, round_id: str) -> bool: - """Check that the round has finished.""" - return self.check_in_last_round(round_id) - - def check_round_height_has_changed(self, round_height: int) -> bool: - """Check that the round height has changed.""" - return self.round_sequence.current_round_height != round_height - - def is_round_ended(self, round_id: str) -> Callable[[], bool]: - """Get a callable to check whether the current round has ended.""" - return partial(self.check_not_in_round, round_id) - - def wait_until_round_end( - self, timeout: Optional[float] = None - ) -> Generator[None, None, None]: - """ - Wait until the ABCI application exits from a round. - - :param timeout: the timeout for the wait - :yield: None - """ - round_id = self.matching_round.auto_round_id() - round_height = self.round_sequence.current_round_height - if self.check_not_in_round(round_id) and self.check_not_in_last_round(round_id): - raise ValueError( - f"Should be in matching round ({round_id}) or last round ({self.round_sequence.last_round_id}), " - f"actual round {self.round_sequence.current_round_id}!" - ) - yield from self.wait_for_condition( - partial(self.check_round_height_has_changed, round_height), timeout=timeout - ) - - def wait_from_last_timestamp(self, seconds: float) -> Any: - """ - Delay execution for a given number of seconds from the last timestamp. - - The argument may be a floating point number for subsecond precision. - This is a local method that does not depend on the global clock, - so the usage of datetime.now() is acceptable here. - - :param seconds: the seconds - :yield: None - """ - if seconds < 0: - raise ValueError("Can only wait for a positive amount of time") - deadline = self.round_sequence.abci_app.last_timestamp + datetime.timedelta( - seconds=seconds - ) - - def _wait_until() -> bool: - return datetime.datetime.now() > deadline - - yield from self.wait_for_condition(_wait_until) - - def is_done(self) -> bool: - """Check whether the behaviour is done.""" - return self._is_done - - def set_done(self) -> None: - """Set the behaviour to done.""" - self._is_done = True - - def send_a2a_transaction( - self, payload: BaseTxPayload, resetting: bool = False - ) -> Generator: - """ - Send transaction and wait for the response, and repeat until not successful. - - :param: payload: the payload to send - :param: resetting: flag indicating if we are resetting Tendermint nodes in this round. - :yield: the responses - """ - stop_condition = self.is_round_ended(self.matching_round.auto_round_id()) - round_count = self.synchronized_data.round_count - object.__setattr__(payload, "round_count", round_count) - yield from self._send_transaction( - payload, - resetting, - stop_condition=stop_condition, - ) - - def async_act_wrapper(self) -> Generator: - """Do the act, supporting asynchronous execution.""" - if not self._is_started: - self._log_start() - self._is_started = True - try: - if self.round_sequence.syncing_up: - yield from self._check_sync() - else: - yield from self.async_act() - except StopIteration: - self.clean_up() - self.set_done() - self._log_end() - return - - if self._is_done: - self._log_end() - - def _check_sync( - self, - ) -> Generator[None, None, None]: - """Check if agent has completed sync.""" - self.context.logger.info("Synchronizing with Tendermint...") - for _ in range(self.context.params.tendermint_max_retries): - self.context.logger.debug( - "Checking status @ " + self.context.params.tendermint_url + "/status", - ) - status = yield from self._get_status() - try: - json_body = json.loads(status.body.decode()) - remote_height = int( - json_body["result"]["sync_info"]["latest_block_height"] - ) - local_height = int(self.round_sequence.height) - _is_sync_complete = local_height == remote_height - if _is_sync_complete: - self.context.logger.info( - f"local height == remote == {local_height}; Synchronization complete." - ) - self.round_sequence.end_sync() - # we set the block stall deadline here because we pinged the /status endpoint - # and received a response from tm, which means that the communication is fine - self.round_sequence.set_block_stall_deadline() - return - yield from self.sleep(self.context.params.tendermint_check_sleep_delay) - except (json.JSONDecodeError, KeyError): # pragma: nocover - self.context.logger.debug( - "Tendermint not accepting transactions yet, trying again!" - ) - yield from self.sleep(self.context.params.tendermint_check_sleep_delay) - - self.context.logger.error("Could not synchronize with Tendermint!") - - def _log_start(self) -> None: - """Log the entering in the behaviour.""" - self.context.logger.info(f"Entered in the '{self.name}' behaviour") - - def _log_end(self) -> None: - """Log the exiting from the behaviour.""" - self.context.logger.info(f"'{self.name}' behaviour is done") - - @classmethod - def _get_request_nonce_from_dialogue(cls, dialogue: Dialogue) -> str: - """Get the request nonce for the request, from the protocol's dialogue.""" - return dialogue.dialogue_label.dialogue_reference[0] - - def _send_transaction( # pylint: disable=too-many-arguments,too-many-locals,too-many-statements - self, - payload: BaseTxPayload, - resetting: bool = False, - stop_condition: Callable[[], bool] = lambda: False, - request_timeout: Optional[float] = None, - request_retry_delay: Optional[float] = None, - tx_timeout: Optional[float] = None, - max_attempts: Optional[int] = None, - ) -> Generator: - """ - Send transaction and wait for the response, repeat until not successful. - - Steps: - - Request the signature of the payload to the Decision Maker - - Send the transaction to the 'price-estimation' app via the Tendermint - node, and wait/repeat until the transaction is not mined. - - Happy-path full flow of the messages. - - get_signature: - AbstractRoundAbci skill -> (SigningMessage | SIGN_MESSAGE) -> DecisionMaker - DecisionMaker -> (SigningMessage | SIGNED_MESSAGE) -> AbstractRoundAbci skill - - _submit_tx: - AbstractRoundAbci skill -> (HttpMessage | REQUEST) -> Http client connection - Http client connection -> (HttpMessage | RESPONSE) -> AbstractRoundAbci skill - - _wait_until_transaction_delivered: - AbstractRoundAbci skill -> (HttpMessage | REQUEST) -> Http client connection - Http client connection -> (HttpMessage | RESPONSE) -> AbstractRoundAbci skill - - :param: payload: the payload to send - :param: resetting: flag indicating if we are resetting Tendermint nodes in this round. - :param: stop_condition: the condition to be checked to interrupt the - waiting loop. - :param: request_timeout: the timeout for the requests - :param: request_retry_delay: the delay to wait after failed requests - :param: tx_timeout: the timeout to wait for tx delivery - :param: max_attempts: max retry attempts - :yield: the responses - """ - request_timeout = ( - self.params.request_timeout if request_timeout is None else request_timeout - ) - request_retry_delay = ( - self.params.request_retry_delay - if request_retry_delay is None - else request_retry_delay - ) - tx_timeout = self.params.tx_timeout if tx_timeout is None else tx_timeout - max_attempts = ( - self.params.max_attempts if max_attempts is None else max_attempts - ) - while not stop_condition(): - self.context.logger.debug( - f"Trying to send payload: {pprint.pformat(payload.json)}" - ) - signature_bytes = yield from self.get_signature(payload.encode()) - transaction = Transaction(payload, signature_bytes) - try: - response = yield from self._submit_tx( - transaction.encode(), timeout=request_timeout - ) - # There is no guarantee that beyond this line will be executed for a given behaviour execution. - # The tx could lead to a round transition which exits us from the behaviour execution. - # It's unlikely to happen anywhere before line 538 but there it is very likely to not - # yield in time before the behaviour is finished. As a result logs below might not show - # up on the happy execution path. - except TimeoutException: - self.context.logger.warning( - f"Timeout expired for submit tx. Retrying in {request_retry_delay} seconds..." - ) - payload = payload.with_new_id() - yield from self.sleep(request_retry_delay) - continue - response = cast(HttpMessage, response) - non_200_code = not self._check_http_return_code_200(response) - if non_200_code and ( - self._non_200_return_code_count - > NON_200_RETURN_CODE_DURING_RESET_THRESHOLD - or not resetting - ): - self.context.logger.error( - f"Received return code != 200 with response {response} with body {str(response.body)}. " - f"Retrying in {request_retry_delay} seconds..." - ) - elif non_200_code and resetting: - self._non_200_return_code_count += 1 - if non_200_code: - payload = payload.with_new_id() - yield from self.sleep(request_retry_delay) - continue - try: - json_body = json.loads(response.body) - except json.JSONDecodeError as e: # pragma: nocover - raise ValueError( - f"Unable to decode response: {response} with body {str(response.body)}" - ) from e - self.context.logger.debug(f"JSON response: {pprint.pformat(json_body)}") - tx_hash = json_body["result"]["hash"] - if json_body["result"]["code"] != OK_CODE: - self.context.logger.error( - f"Received tendermint code != 0. Retrying in {request_retry_delay} seconds..." - ) - yield from self.sleep(request_retry_delay) - continue # pragma: nocover - - try: - is_delivered, res = yield from self._wait_until_transaction_delivered( - tx_hash, - timeout=tx_timeout, - max_attempts=max_attempts, - request_retry_delay=request_retry_delay, - ) - except TimeoutException: - self.context.logger.warning( - f"Timeout expired for wait until transaction delivered. " - f"Retrying in {request_retry_delay} seconds..." - ) - payload = payload.with_new_id() - yield from self.sleep(request_retry_delay) - continue # pragma: nocover - - if is_delivered: - self.context.logger.debug("A2A transaction delivered!") - break - if isinstance(res, HttpMessage) and self._is_invalid_transaction(res): - self.context.logger.error( - f"Tx sent but not delivered. Invalid transaction - not trying again! Response = {res}" - ) - break - # otherwise, repeat until done, or until stop condition is true - if isinstance(res, HttpMessage) and self._tx_not_found(tx_hash, res): - self.context.logger.warning(f"Tx {tx_hash} not found! Response = {res}") - else: - self.context.logger.warning( - f"Tx sent but not delivered. Response = {res}" - ) - payload = payload.with_new_id() - self.context.logger.debug( - "Stop condition is true, no more attempts to send the transaction." - ) - - @staticmethod - def _is_invalid_transaction(res: HttpMessage) -> bool: - """Check if the transaction is invalid.""" - try: - error_codes = ["TransactionNotValidError"] - body_ = json.loads(res.body) - return any( - [error_code in body_["tx_result"]["info"] for error_code in error_codes] - ) - except Exception: # pylint: disable=broad-except # pragma: nocover - return False - - @staticmethod - def _tx_not_found(tx_hash: str, res: HttpMessage) -> bool: - """Check if the transaction could not be found.""" - try: - error = json.loads(res.body)["error"] - not_found_field_to_text = { - "code": -32603, - "message": "Internal error", - "data": f"tx ({tx_hash}) not found", - } - return all( - [ - text == error[field] - for field, text in not_found_field_to_text.items() - ] - ) - except Exception: # pylint: disable=broad-except # pragma: nocover - return False - - def _send_signing_request( - self, raw_message: bytes, is_deprecated_mode: bool = False - ) -> None: - """ - Send a signing request. - - Happy-path full flow of the messages. - - AbstractRoundAbci skill -> (SigningMessage | SIGN_MESSAGE) -> DecisionMaker - DecisionMaker -> (SigningMessage | SIGNED_MESSAGE) -> AbstractRoundAbci skill - - :param raw_message: raw message bytes - :param is_deprecated_mode: is deprecated flag. - """ - signing_dialogues = cast(SigningDialogues, self.context.signing_dialogues) - signing_msg, signing_dialogue = signing_dialogues.create( - counterparty=self.context.decision_maker_address, - performative=SigningMessage.Performative.SIGN_MESSAGE, - raw_message=RawMessage( - self.context.default_ledger_id, - raw_message, - is_deprecated_mode=is_deprecated_mode, - ), - terms=Terms( - ledger_id=self.context.default_ledger_id, - sender_address="", - counterparty_address="", - amount_by_currency_id={}, - quantities_by_good_id={}, - nonce="", - ), - ) - request_nonce = self._get_request_nonce_from_dialogue(signing_dialogue) - cast(Requests, self.context.requests).request_id_to_callback[ - request_nonce - ] = self.get_callback_request() - self.context.decision_maker_message_queue.put_nowait(signing_msg) - - def _send_transaction_signing_request( - self, raw_transaction: RawTransaction, terms: Terms - ) -> None: - """ - Send a transaction signing request. - - Happy-path full flow of the messages. - - AbstractRoundAbci skill -> (SigningMessage | SIGN_TRANSACTION) -> DecisionMaker - DecisionMaker -> (SigningMessage | SIGNED_TRANSACTION) -> AbstractRoundAbci skill - - :param raw_transaction: raw transaction data - :param terms: signing terms - """ - signing_dialogues = cast(SigningDialogues, self.context.signing_dialogues) - signing_msg, signing_dialogue = signing_dialogues.create( - counterparty=self.context.decision_maker_address, - performative=SigningMessage.Performative.SIGN_TRANSACTION, - raw_transaction=raw_transaction, - terms=terms, - ) - request_nonce = self._get_request_nonce_from_dialogue(signing_dialogue) - cast(Requests, self.context.requests).request_id_to_callback[ - request_nonce - ] = self.get_callback_request() - self.context.decision_maker_message_queue.put_nowait(signing_msg) - - def _send_transaction_request( - self, - signing_msg: SigningMessage, - use_flashbots: bool = False, - target_block_numbers: Optional[List[int]] = None, - chain_id: Optional[str] = None, - raise_on_failed_simulation: bool = False, - ) -> None: - """ - Send transaction request. - - Happy-path full flow of the messages. - - AbstractRoundAbci skill -> (LedgerApiMessage | SEND_SIGNED_TRANSACTION) -> Ledger connection - Ledger connection -> (LedgerApiMessage | TRANSACTION_DIGEST) -> AbstractRoundAbci skill - - :param signing_msg: signing message - :param use_flashbots: whether to use flashbots for the transaction or not - :param target_block_numbers: the target block numbers in case we are using flashbots - :param chain_id: the chain name to use for the ledger call - :param raise_on_failed_simulation: whether to raise an exception if the simulation fails or not. - """ - ledger_api_dialogues = cast( - LedgerApiDialogues, self.context.ledger_api_dialogues - ) - - create_kwargs: Dict[ - str, Union[str, SignedTransaction, List[SignedTransaction]] - ] = dict( - counterparty=LEDGER_API_ADDRESS, - performative=LedgerApiMessage.Performative.SEND_SIGNED_TRANSACTION, - ) - if chain_id: - kwargs = LedgerApiMessage.Kwargs({"chain_id": chain_id}) - create_kwargs.update(dict(kwargs=kwargs)) - - if use_flashbots: - _kwargs = { - "chain_id": chain_id, - "raise_on_failed_simulation": raise_on_failed_simulation, - "use_all_builders": True, # TODO: make this a proper parameter - } - if target_block_numbers is not None: - _kwargs["target_block_numbers"] = target_block_numbers # type: ignore - create_kwargs.update( - dict( - performative=LedgerApiMessage.Performative.SEND_SIGNED_TRANSACTIONS, - # we do not support sending multiple signed txs and receiving multiple tx hashes yet - signed_transactions=LedgerApiMessage.SignedTransactions( - ledger_id=FLASHBOTS_LEDGER_ID, - signed_transactions=[signing_msg.signed_transaction.body], - ), - kwargs=LedgerApiMessage.Kwargs(_kwargs), - ) - ) - else: - create_kwargs.update( - dict( - signed_transaction=signing_msg.signed_transaction, - ) - ) - - ledger_api_msg, ledger_api_dialogue = ledger_api_dialogues.create( - **create_kwargs - ) - ledger_api_dialogue = cast(LedgerApiDialogue, ledger_api_dialogue) - request_nonce = self._get_request_nonce_from_dialogue(ledger_api_dialogue) - cast(Requests, self.context.requests).request_id_to_callback[ - request_nonce - ] = self.get_callback_request() - self.context.outbox.put_message(message=ledger_api_msg) - self.context.logger.debug("Sending transaction to ledger...") - - def _send_transaction_receipt_request( - self, - tx_digest: str, - retry_timeout: Optional[int] = None, - retry_attempts: Optional[int] = None, - **kwargs: Any, - ) -> None: - """ - Send transaction receipt request. - - Happy-path full flow of the messages. - - AbstractRoundAbci skill -> (LedgerApiMessage | GET_TRANSACTION_RECEIPT) -> Ledger connection - Ledger connection -> (LedgerApiMessage | TRANSACTION_RECEIPT) -> AbstractRoundAbci skill - - :param tx_digest: transaction digest string - :param retry_timeout: retry timeout in seconds - :param retry_attempts: number of retry attempts - """ - ledger_api_dialogues = cast( - LedgerApiDialogues, self.context.ledger_api_dialogues - ) - ledger_api_msg, ledger_api_dialogue = ledger_api_dialogues.create( - counterparty=LEDGER_API_ADDRESS, - performative=LedgerApiMessage.Performative.GET_TRANSACTION_RECEIPT, - transaction_digest=LedgerApiMessage.TransactionDigest( - ledger_id=self.context.default_ledger_id, body=tx_digest - ), - retry_timeout=retry_timeout, - retry_attempts=retry_attempts, - kwargs=LedgerApiMessage.Kwargs(kwargs), - ) - ledger_api_dialogue = cast(LedgerApiDialogue, ledger_api_dialogue) - request_nonce = self._get_request_nonce_from_dialogue(ledger_api_dialogue) - cast(Requests, self.context.requests).request_id_to_callback[ - request_nonce - ] = self.get_callback_request() - self.context.outbox.put_message(message=ledger_api_msg) - self.context.logger.debug( - f"Sending transaction receipt request for tx_digest='{tx_digest}'..." - ) - - def _handle_signing_failure(self) -> None: - """Handle signing failure.""" - self.context.logger.error("The transaction could not be signed!") - - def _submit_tx( - self, tx_bytes: bytes, timeout: Optional[float] = None - ) -> Generator[None, None, HttpMessage]: - """Send a broadcast_tx_sync request. - - Happy-path full flow of the messages. - - _do_request: - AbstractRoundAbci skill -> (HttpMessage | REQUEST) -> Http client connection - Http client connection -> (HttpMessage | RESPONSE) -> AbstractRoundAbci skill - - :param tx_bytes: transaction bytes - :param timeout: timeout seconds - :yield: HttpMessage object - :return: http response - """ - request_message, http_dialogue = self._build_http_request_message( - "GET", - self.context.params.tendermint_url - + f"/broadcast_tx_sync?tx=0x{tx_bytes.hex()}", - ) - result = yield from self._do_request( - request_message, http_dialogue, timeout=timeout - ) - return result - - def _get_tx_info( - self, tx_hash: str, timeout: Optional[float] = None - ) -> Generator[None, None, HttpMessage]: - """ - Get transaction info from tx hash. - - Happy-path full flow of the messages. - - _do_request: - AbstractRoundAbci skill -> (HttpMessage | REQUEST) -> Http client connection - Http client connection -> (HttpMessage | RESPONSE) -> AbstractRoundAbci skill - - :param tx_hash: transaction hash - :param timeout: timeout in seconds - :yield: HttpMessage object - :return: http response - """ - request_message, http_dialogue = self._build_http_request_message( - "GET", - self.context.params.tendermint_url + f"/tx?hash=0x{tx_hash}", - ) - result = yield from self._do_request( - request_message, http_dialogue, timeout=timeout - ) - return result - - def _get_status(self) -> Generator[None, None, HttpMessage]: - """ - Get Tendermint node's status. - - Happy-path full flow of the messages. - - _do_request: - AbstractRoundAbci skill -> (HttpMessage | REQUEST) -> Http client connection - Http client connection -> (HttpMessage | RESPONSE) -> AbstractRoundAbci skill - - :yield: HttpMessage object - :return: http response from tendermint - """ - request_message, http_dialogue = self._build_http_request_message( - "GET", - self.context.params.tendermint_url + "/status", - ) - result = yield from self._do_request(request_message, http_dialogue) - return result - - def _get_netinfo( - self, timeout: Optional[float] = None - ) -> Generator[None, None, HttpMessage]: - """Makes a GET request to it's tendermint node's /net_info endpoint.""" - request_message, http_dialogue = self._build_http_request_message( - method="GET", url=f"{self.context.params.tendermint_url}/net_info" - ) - result = yield from self._do_request(request_message, http_dialogue, timeout) - return result - - def num_active_peers( - self, timeout: Optional[float] = None - ) -> Generator[None, None, Optional[int]]: - """Returns the number of active peers in the network.""" - try: - http_response = yield from self._get_netinfo(timeout) - http_ok = 200 - if http_response.status_code != http_ok: - # a bad response was received, we cannot retrieve the number of active peers - self.context.logger.warning( - f"`/net_info` responded with status {http_response.status_code}." - ) - return None - - res_body = json.loads(http_response.body) - num_peers_str = res_body.get("result", {}).get("n_peers", None) - if num_peers_str is None: - return None - num_peers = int(num_peers_str) - # num_peers hold the number of peers the tm node we are - # making the TX to currently has an active connection - # we add 1 because the node we are making the request through - # is not accounted for in this number - return num_peers + 1 - except TimeoutException: - self.context.logger.warning( - f"Couldn't retrieve `/net_info` response in {timeout}s." - ) - return None - - def get_callback_request(self) -> Callable[[Message, "BaseBehaviour"], None]: - """Wrapper for callback request which depends on whether the message has not been handled on time. - - :return: the request callback. - """ - - def callback_request( - message: Message, current_behaviour: BaseBehaviour - ) -> None: - """The callback request.""" - if self.is_stopped: - self.context.logger.debug( - "Dropping message as behaviour has stopped: %s", message - ) - elif self != current_behaviour: - self.handle_late_messages(self.behaviour_id, message) - elif self.state == AsyncBehaviour.AsyncState.WAITING_MESSAGE: - self.try_send(message) - else: - self.context.logger.warning( - "Could not send message to FSMBehaviour: %s", message - ) - - return callback_request - - def get_http_response( - self, - method: str, - url: str, - content: Optional[bytes] = None, - headers: Optional[Dict[str, str]] = None, - parameters: Optional[Dict[str, str]] = None, - ) -> Generator[None, None, HttpMessage]: - """ - Send an http request message from the skill context. - - This method is skill-specific, and therefore - should not be used elsewhere. - - Happy-path full flow of the messages. - - _do_request: - AbstractRoundAbci skill -> (HttpMessage | REQUEST) -> Http client connection - Http client connection -> (HttpMessage | RESPONSE) -> AbstractRoundAbci skill - - :param method: the http request method (i.e. 'GET' or 'POST'). - :param url: the url to send the message to. - :param content: the payload. - :param headers: headers to be included. - :param parameters: url query parameters. - :yield: HttpMessage object - :return: the http message and the http dialogue - """ - http_message, http_dialogue = self._build_http_request_message( - method=method, - url=url, - content=content, - headers=headers, - parameters=parameters, - ) - response = yield from self._do_request(http_message, http_dialogue) - return response - - def _do_request( - self, - request_message: HttpMessage, - http_dialogue: HttpDialogue, - timeout: Optional[float] = None, - ) -> Generator[None, None, HttpMessage]: - """ - Do a request and wait the response, asynchronously. - - Happy-path full flow of the messages. - - AbstractRoundAbci skill -> (HttpMessage | REQUEST) -> Http client connection - Http client connection -> (HttpMessage | RESPONSE) -> AbstractRoundAbci skill - - :param request_message: The request message - :param http_dialogue: the HTTP dialogue associated to the request - :param timeout: seconds to wait for the reply. - :yield: HttpMessage object - :return: the response message - """ - self.context.outbox.put_message(message=request_message) - request_nonce = self._get_request_nonce_from_dialogue(http_dialogue) - cast(Requests, self.context.requests).request_id_to_callback[ - request_nonce - ] = self.get_callback_request() - # notify caller by propagating potential timeout exception. - response = yield from self.wait_for_message(timeout=timeout) - return response - - def _build_http_request_message( - self, - method: str, - url: str, - content: Optional[bytes] = None, - headers: Optional[Dict[str, str]] = None, - parameters: Optional[Dict[str, str]] = None, - ) -> Tuple[HttpMessage, HttpDialogue]: - """ - Send an http request message from the skill context. - - This method is skill-specific, and therefore - should not be used elsewhere. - - :param method: the http request method (i.e. 'GET' or 'POST'). - :param url: the url to send the message to. - :param content: the payload. - :param headers: headers to be included. - :param parameters: url query parameters. - :return: the http message and the http dialogue - """ - if parameters: - url = url + "?" - for key, val in parameters.items(): - url += f"{key}={val}&" - url = url[:-1] - - header_string = "" - if headers: - for key, val in headers.items(): - header_string += f"{key}: {val}\r\n" - - # context - http_dialogues = cast(HttpDialogues, self.context.http_dialogues) - - # http request message - request_http_message, http_dialogue = http_dialogues.create( - counterparty=str(HTTP_CLIENT_PUBLIC_ID), - performative=HttpMessage.Performative.REQUEST, - method=method, - url=url, - headers=header_string, - version="", - body=b"" if content is None else content, - ) - request_http_message = cast(HttpMessage, request_http_message) - http_dialogue = cast(HttpDialogue, http_dialogue) - return request_http_message, http_dialogue - - def _wait_until_transaction_delivered( - self, - tx_hash: str, - timeout: Optional[float] = None, - max_attempts: Optional[int] = None, - request_retry_delay: Optional[float] = None, - ) -> Generator[None, None, Tuple[bool, Optional[HttpMessage]]]: - """ - Wait until transaction is delivered. - - Happy-path full flow of the messages. - - _get_tx_info: - AbstractRoundAbci skill -> (HttpMessage | REQUEST) -> Http client connection - Http client connection -> (HttpMessage | RESPONSE) -> AbstractRoundAbci skill - - This is a local method that does not depend on the global clock, - so the usage of datetime.now() is acceptable here. - - :param tx_hash: the transaction hash to check. - :param timeout: timeout - :param: request_retry_delay: the delay to wait after failed requests - :param: max_attempts: the maximun number of attempts - :yield: None - :return: True if it is delivered successfully, False otherwise - """ - if timeout is not None: - deadline = datetime.datetime.now() + datetime.timedelta(0, timeout) - else: - deadline = datetime.datetime.max - request_retry_delay = ( - self.params.request_retry_delay - if request_retry_delay is None - else request_retry_delay - ) - max_attempts = ( - self.params.max_attempts if max_attempts is None else max_attempts - ) - - response = None - for _ in range(max_attempts): - request_timeout = ( - (deadline - datetime.datetime.now()).total_seconds() - if timeout is not None - else None - ) - if request_timeout is not None and request_timeout < 0: - raise TimeoutException() - - response = yield from self._get_tx_info(tx_hash, timeout=request_timeout) - if response.status_code != 200: - yield from self.sleep(request_retry_delay) - continue - - try: - json_body = json.loads(response.body) - except json.JSONDecodeError as e: # pragma: nocover - raise ValueError( - f"Unable to decode response: {response} with body {str(response.body)}" - ) from e - tx_result = json_body["result"]["tx_result"] - return tx_result["code"] == OK_CODE, response - - return False, response - - @classmethod - def _check_http_return_code_200(cls, response: HttpMessage) -> bool: - """Check the HTTP response has return code 200.""" - return response.status_code == 200 - - def _get_default_terms(self) -> Terms: - """ - Get default transaction terms. - - :return: terms - """ - terms = Terms( - ledger_id=self.context.default_ledger_id, - sender_address=self.context.agent_address, - counterparty_address=self.context.agent_address, - amount_by_currency_id={}, - quantities_by_good_id={}, - nonce="", - ) - return terms - - def get_signature( - self, message: bytes, is_deprecated_mode: bool = False - ) -> Generator[None, None, str]: - """ - Get signature for message. - - Happy-path full flow of the messages. - - _send_signing_request: - AbstractRoundAbci skill -> (SigningMessage | SIGN_MESSAGE) -> DecisionMaker - DecisionMaker -> (SigningMessage | SIGNED_MESSAGE) -> AbstractRoundAbci skill - - :param message: message bytes - :param is_deprecated_mode: is deprecated mode flag - :yield: SigningMessage object - :return: message signature - """ - self._send_signing_request(message, is_deprecated_mode) - signature_response = yield from self.wait_for_message() - signature_response = cast(SigningMessage, signature_response) - if signature_response.performative == SigningMessage.Performative.ERROR: - self._handle_signing_failure() - raise RuntimeError("Internal error: failure during signing.") - signature_bytes = signature_response.signed_message.body - return signature_bytes - - def send_raw_transaction( - self, - transaction: RawTransaction, - use_flashbots: bool = False, - target_block_numbers: Optional[List[int]] = None, - raise_on_failed_simulation: bool = False, - chain_id: Optional[str] = None, - ) -> Generator[ - None, - Union[None, SigningMessage, LedgerApiMessage], - Tuple[Optional[str], RPCResponseStatus], - ]: - """ - Send raw transactions to the ledger for mining. - - Happy-path full flow of the messages. - - _send_transaction_signing_request: - AbstractRoundAbci skill -> (SigningMessage | SIGN_TRANSACTION) -> DecisionMaker - DecisionMaker -> (SigningMessage | SIGNED_TRANSACTION) -> AbstractRoundAbci skill - - _send_transaction_request: - AbstractRoundAbci skill -> (LedgerApiMessage | SEND_SIGNED_TRANSACTION) -> Ledger connection - Ledger connection -> (LedgerApiMessage | TRANSACTION_DIGEST) -> AbstractRoundAbci skill - - :param transaction: transaction data - :param use_flashbots: whether to use flashbots for the transaction or not - :param target_block_numbers: the target block numbers in case we are using flashbots - :param raise_on_failed_simulation: whether to raise an exception if the transaction fails the simulation or not - :param chain_id: the chain name to use for the ledger call - :yield: SigningMessage object - :return: transaction hash - """ - if chain_id is None: - chain_id = self.params.default_chain_id - - terms = Terms( - chain_id, - self.context.agent_address, - counterparty_address="", - amount_by_currency_id={}, - quantities_by_good_id={}, - nonce="", - ) - self.context.logger.info( - f"Sending signing request to ledger '{chain_id}' for transaction: {transaction}..." - ) - self._send_transaction_signing_request(transaction, terms) - signature_response = yield from self.wait_for_message() - signature_response = cast(SigningMessage, signature_response) - tx_hash_backup = signature_response.signed_transaction.body.get("hash") - if ( - signature_response.performative - != SigningMessage.Performative.SIGNED_TRANSACTION - ): - self.context.logger.error( - f"Error when requesting transaction signature: {signature_response}" - ) - return None, RPCResponseStatus.UNCLASSIFIED_ERROR - self.context.logger.info( - f"Received signature response: {signature_response}\n Sending transaction..." - ) - self._send_transaction_request( - signature_response, - use_flashbots, - target_block_numbers, - chain_id, - raise_on_failed_simulation, - ) - transaction_digest_msg = yield from self.wait_for_message() - transaction_digest_msg = cast(LedgerApiMessage, transaction_digest_msg) - performative = transaction_digest_msg.performative - if performative not in ( - LedgerApiMessage.Performative.TRANSACTION_DIGEST, - LedgerApiMessage.Performative.TRANSACTION_DIGESTS, - ): - error = f"Error when requesting transaction digest: {transaction_digest_msg.message}" - self.context.logger.error(error) - return tx_hash_backup, self.__parse_rpc_error(error) - - tx_hash = ( - # we do not support sending multiple messages and receiving multiple tx hashes yet - transaction_digest_msg.transaction_digests.transaction_digests[0] - if performative == LedgerApiMessage.Performative.TRANSACTION_DIGESTS - else transaction_digest_msg.transaction_digest.body - ) - - self.context.logger.info( - f"Transaction sent! Received transaction digest: {tx_hash}" - ) - - if tx_hash != tx_hash_backup: - # this should never happen - self.context.logger.error( - f"Unexpected error! The signature response's hash `{tx_hash_backup}` " - f"does not match the one received from the transaction response `{tx_hash}`!" - ) - return None, RPCResponseStatus.UNCLASSIFIED_ERROR - - return tx_hash, RPCResponseStatus.SUCCESS - - def get_transaction_receipt( - self, - tx_digest: str, - retry_timeout: Optional[int] = None, - retry_attempts: Optional[int] = None, - chain_id: Optional[str] = None, - ) -> Generator[None, None, Optional[Dict]]: - """ - Get transaction receipt. - - Happy-path full flow of the messages. - - _send_transaction_receipt_request: - AbstractRoundAbci skill -> (LedgerApiMessage | GET_TRANSACTION_RECEIPT) -> Ledger connection - Ledger connection -> (LedgerApiMessage | TRANSACTION_RECEIPT) -> AbstractRoundAbci skill - - :param tx_digest: transaction digest received from raw transaction. - :param retry_timeout: retry timeout. - :param retry_attempts: number of retry attempts allowed. - :yield: LedgerApiMessage object - :return: transaction receipt data - """ - if chain_id is None: - chain_id = self.params.default_chain_id - self._send_transaction_receipt_request( - tx_digest, retry_timeout, retry_attempts, chain_id=chain_id - ) - transaction_receipt_msg = yield from self.wait_for_message() - if ( - transaction_receipt_msg.performative == LedgerApiMessage.Performative.ERROR - ): # pragma: nocover - self.context.logger.error( - f"Error when requesting transaction receipt: {transaction_receipt_msg.message}" - ) - return None - tx_receipt = transaction_receipt_msg.transaction_receipt.receipt - return tx_receipt - - def get_ledger_api_response( - self, - performative: LedgerApiMessage.Performative, - ledger_callable: str, - **kwargs: Any, - ) -> Generator[None, None, LedgerApiMessage]: - """ - Request data from ledger api - - Happy-path full flow of the messages. - - AbstractRoundAbci skill -> (LedgerApiMessage | LedgerApiMessage.Performative) -> Ledger connection - Ledger connection -> (LedgerApiMessage | LedgerApiMessage.Performative) -> AbstractRoundAbci skill - - :param performative: the message performative - :param ledger_callable: the callable to call on the contract - :param kwargs: keyword argument for the contract api request - :return: the contract api response - :yields: the contract api response - """ - ledger_api_dialogues = cast( - LedgerApiDialogues, self.context.ledger_api_dialogues - ) - kwargs = { - "performative": performative, - "counterparty": LEDGER_API_ADDRESS, - "ledger_id": self.context.default_ledger_id, - "callable": ledger_callable, - "kwargs": LedgerApiMessage.Kwargs(kwargs), - "args": tuple(), - } - ledger_api_msg, ledger_api_dialogue = ledger_api_dialogues.create(**kwargs) - ledger_api_dialogue = cast( - LedgerApiDialogue, - ledger_api_dialogue, - ) - ledger_api_dialogue.terms = self._get_default_terms() - request_nonce = self._get_request_nonce_from_dialogue(ledger_api_dialogue) - cast(Requests, self.context.requests).request_id_to_callback[ - request_nonce - ] = self.get_callback_request() - self.context.outbox.put_message(message=ledger_api_msg) - response = yield from self.wait_for_message() - return response - - def get_contract_api_response( - self, - performative: ContractApiMessage.Performative, - contract_address: Optional[str], - contract_id: str, - contract_callable: str, - ledger_id: Optional[str] = None, - **kwargs: Any, - ) -> Generator[None, None, ContractApiMessage]: - """ - Request contract safe transaction hash - - Happy-path full flow of the messages. - - AbstractRoundAbci skill -> (ContractApiMessage | ContractApiMessage.Performative) -> Ledger connection (contract dispatcher) - Ledger connection (contract dispatcher) -> (ContractApiMessage | ContractApiMessage.Performative) -> AbstractRoundAbci skill - - :param performative: the message performative - :param contract_address: the contract address - :param contract_id: the contract id - :param contract_callable: the callable to call on the contract - :param ledger_id: the ledger id, if not specified, the default ledger id is used - :param kwargs: keyword argument for the contract api request - :return: the contract api response - :yields: the contract api response - """ - contract_api_dialogues = cast( - ContractApiDialogues, self.context.contract_api_dialogues - ) - kwargs = { - "performative": performative, - "counterparty": LEDGER_API_ADDRESS, - "ledger_id": ledger_id or self.context.default_ledger_id, - "contract_id": contract_id, - "callable": contract_callable, - "kwargs": ContractApiMessage.Kwargs(kwargs), - } - if contract_address is not None: - kwargs["contract_address"] = contract_address - contract_api_msg, contract_api_dialogue = contract_api_dialogues.create( - **kwargs - ) - contract_api_dialogue = cast( - ContractApiDialogue, - contract_api_dialogue, - ) - contract_api_dialogue.terms = self._get_default_terms() - request_nonce = self._get_request_nonce_from_dialogue(contract_api_dialogue) - cast(Requests, self.context.requests).request_id_to_callback[ - request_nonce - ] = self.get_callback_request() - self.context.outbox.put_message(message=contract_api_msg) - response = yield from self.wait_for_message() - return response - - @staticmethod - def __parse_rpc_error(error: str) -> RPCResponseStatus: - """Parse an RPC error and return an `RPCResponseStatus`""" - if "replacement transaction underpriced" in error: - return RPCResponseStatus.UNDERPRICED - if "nonce too low" in error: - return RPCResponseStatus.INCORRECT_NONCE - if "insufficient funds" in error: - return RPCResponseStatus.INSUFFICIENT_FUNDS - if "already known" in error: - return RPCResponseStatus.ALREADY_KNOWN - if "Simulation failed for bundle" in error: - return RPCResponseStatus.SIMULATION_FAILED - return RPCResponseStatus.UNCLASSIFIED_ERROR - - def _acn_request_from_pending( - self, performative: TendermintMessage.Performative - ) -> Generator: - """Perform an ACN request to each one of the agents which have not sent a response yet.""" - not_responded_yet = { - address - for address, deliverable in self.shared_state.address_to_acn_deliverable.items() - if deliverable is None - } - - if len(not_responded_yet) == 0: - return - - self.context.logger.debug(f"Need ACN response from {not_responded_yet}.") - for address in not_responded_yet: - self.context.logger.debug(f"Sending ACN request to {address}.") - dialogues = cast(TendermintDialogues, self.context.tendermint_dialogues) - message, _ = dialogues.create( - counterparty=address, performative=performative - ) - message = cast(TendermintMessage, message) - context = EnvelopeContext(connection_id=P2P_LIBP2P_CLIENT_PUBLIC_ID) - self.context.outbox.put_message(message=message, context=context) - - # we wait for the `address_to_acn_deliverable` to be populated with the responses (done by the tm handler) - yield from self.sleep(self.params.sleep_time) - - def _perform_acn_request( - self, performative: TendermintMessage.Performative - ) -> Generator[None, None, Any]: - """Perform an ACN request. - - Waits `sleep_time` to receive a common response from the majority of the agents. - Retries `max_attempts` times only for the agents which have not responded yet. - - :param performative: the ACN request performative. - :return: the result that the majority of the agents sent. If majority cannot be reached, returns `None`. - """ - # reset the ACN deliverables at the beginning of a new request - self.shared_state.address_to_acn_deliverable = self.shared_state.acn_container() - - result = None - for i in range(self.params.max_attempts): - self.context.logger.debug( - f"ACN attempt {i + 1}/{self.params.max_attempts}." - ) - yield from self._acn_request_from_pending(performative) - - result = self.shared_state.get_acn_result() - if result is not None: - break - - return result - - def request_recovery_params(self, should_log: bool) -> Generator[None, None, bool]: - """Request the Tendermint recovery parameters from the other agents via the ACN.""" - - if should_log: - self.context.logger.info( - "Requesting the Tendermint recovery parameters from the other agents via the ACN..." - ) - - performative = TendermintMessage.Performative.GET_RECOVERY_PARAMS - acn_result = yield from self._perform_acn_request(performative) # type: ignore - - if acn_result is None: - if should_log: - self.context.logger.warning( - "No majority has been reached for the Tendermint recovery parameters request via the ACN." - ) - return False - - self.shared_state.tm_recovery_params = acn_result - if should_log: - self.context.logger.info( - f"Updated the Tendermint recovery parameters from the other agents via the ACN: {acn_result}" - ) - return True - - @property - def hard_reset_sleep(self) -> float: - """ - Amount of time to sleep before and after performing a hard reset. - - We sleep for half the reset pause duration as there are no immediate transactions on either side of the reset. - - :returns: the amount of time to sleep in seconds - """ - return self.params.reset_pause_duration / 2 - - def _start_reset(self, on_startup: bool = False) -> Generator: - """ - Start tendermint reset. - - This is a local method that does not depend on the global clock, - so the usage of datetime.now() is acceptable here. - - :param on_startup: Whether we are resetting on the start of the agent. - :yield: None - """ - if self._check_started is None and not self._is_healthy: - if not on_startup: - # if we are on startup we don't need to wait for the reset pause duration - # as the reset is being performed to update the tm config. - yield from self.wait_from_last_timestamp(self.hard_reset_sleep) - self._check_started = datetime.datetime.now() - self._timeout = self.params.max_healthcheck - self._is_healthy = False - yield - - def _end_reset( - self, - ) -> None: - """End tendermint reset. - - This is a local method that does not depend on the global clock, - so the usage of datetime.now() is acceptable here. - """ - self._check_started = None - self._timeout = -1.0 - self._is_healthy = True - - def _is_timeout_expired(self) -> bool: - """Check if the timeout expired. - - This is a local method that does not depend on the global clock, - so the usage of datetime.now() is acceptable here. - - :return: bool - """ - if self._check_started is None or self._is_healthy: - return False - return datetime.datetime.now() > self._check_started + datetime.timedelta( - 0, self._timeout - ) - - def _get_reset_params(self, default: bool) -> Optional[Dict[str, str]]: - """Get the parameters for a hard reset request to Tendermint.""" - if default: - return None - - last_round_transition_timestamp = ( - self.round_sequence.last_round_transition_timestamp - ) - genesis_time = last_round_transition_timestamp.astimezone(pytz.UTC).strftime( - GENESIS_TIME_FMT - ) - return { - "genesis_time": genesis_time, - "initial_height": INITIAL_HEIGHT, - "period_count": str(self.synchronized_data.period_count), - } - - def reset_tendermint_with_wait( # pylint: disable=too-many-locals, too-many-statements - self, - on_startup: bool = False, - is_recovery: bool = False, - ) -> Generator[None, None, bool]: - """ - Performs a hard reset (unsafe-reset-all) on the tendermint node. - - :param on_startup: whether we are resetting on the start of the agent. - :param is_recovery: whether the reset is being performed to recover the agent <-> tm communication. - :yields: None - :returns: whether the reset was successful. - """ - yield from self._start_reset(on_startup=on_startup) - if self._is_timeout_expired(): - # if the Tendermint node cannot update the app then the app cannot work - raise RuntimeError("Error resetting tendermint node.") - - if not self._is_healthy: - self.context.logger.info( - f"Resetting tendermint node at end of period={self.synchronized_data.period_count}." - ) - - backup_blockchain = self.round_sequence.blockchain - self.round_sequence.reset_blockchain() - reset_params = self._get_reset_params(on_startup) - request_message, http_dialogue = self._build_http_request_message( - "GET", - self.params.tendermint_com_url + "/hard_reset", - parameters=reset_params, - ) - result = yield from self._do_request(request_message, http_dialogue) - try: - response = json.loads(result.body.decode()) - if response.get("status"): - self.context.logger.debug(response.get("message")) - self.context.logger.info("Resetting tendermint node successful!") - is_replay = response.get("is_replay", False) - if is_replay: - # in case of replay, the local blockchain should be set up differently. - self.round_sequence.reset_blockchain( - is_replay=is_replay, is_init=True - ) - for handler_name in self.context.handlers.__dict__.keys(): - dialogues = getattr(self.context, f"{handler_name}_dialogues") - dialogues.cleanup() - if not is_recovery: - # in case of successful reset we store the reset params in the shared state, - # so that in the future if the communication with tendermint breaks, and we need to - # perform a hard reset to restore it, we can use these as the right ones - round_count = self.synchronized_data.db.round_count - 1 - # in case we need to reset in order to recover agent <-> tm communication - # we store this round as the one to start from - restart_from_round = self.matching_round - self.shared_state.tm_recovery_params = TendermintRecoveryParams( - reset_params=reset_params, - round_count=round_count, - reset_from_round=restart_from_round.auto_round_id(), - serialized_db_state=self.shared_state.synchronized_data.db.serialize(), - ) - self.round_sequence.abci_app.cleanup( - self.params.cleanup_history_depth, - self.params.cleanup_history_depth_current, - ) - self._end_reset() - - else: - msg = response.get("message") - self.round_sequence.blockchain = backup_blockchain - self.context.logger.error(f"Error resetting: {msg}") - yield from self.sleep(self.params.sleep_time) - return False - except json.JSONDecodeError: - self.context.logger.error( - "Error communicating with tendermint com server." - ) - self.round_sequence.blockchain = backup_blockchain - yield from self.sleep(self.params.sleep_time) - return False - - status = yield from self._get_status() - try: - json_body = json.loads(status.body.decode()) - except json.JSONDecodeError: - self.context.logger.error( - "Tendermint not accepting transactions yet, trying again!" - ) - yield from self.sleep(self.params.sleep_time) - return False - - remote_height = int(json_body["result"]["sync_info"]["latest_block_height"]) - local_height = self.round_sequence.height - if local_height != remote_height: - self.context.logger.warning( - f"local height ({local_height}) != remote height ({remote_height}); retrying..." - ) - yield from self.sleep(self.params.sleep_time) - return False - - self.context.logger.info( - f"local height == remote height == {local_height}; continuing execution..." - ) - if not on_startup: - # if we are on startup we don't need to wait for the reset pause duration - # as the reset is being performed to update the tm config. - yield from self.wait_from_last_timestamp(self.hard_reset_sleep) - self._is_healthy = False - return True - - def send_to_ipfs( # pylint: disable=too-many-arguments - self, - filename: str, - obj: SupportedObjectType, - multiple: bool = False, - filetype: Optional[SupportedFiletype] = None, - custom_storer: Optional[CustomStorerType] = None, - timeout: Optional[float] = None, - **kwargs: Any, - ) -> Generator[None, None, Optional[str]]: - """ - Store an object on IPFS. - - :param filename: the file name to store obj in. If "multiple" is True, filename will be the name of the dir. - :param obj: the object(s) to serialize and store in IPFS as "filename". - :param multiple: whether obj should be stored as multiple files, i.e. directory. - :param filetype: the file type of the object being downloaded. - :param custom_storer: a custom serializer for "obj". - :param timeout: timeout for the request. - :returns: the downloaded object, corresponding to ipfs_hash. - """ - try: - message, dialogue = self._build_ipfs_store_file_req( - filename, - obj, - multiple, - filetype, - custom_storer, - timeout, - **kwargs, - ) - ipfs_message = yield from self._do_ipfs_request(dialogue, message, timeout) - if ipfs_message.performative != IpfsMessage.Performative.IPFS_HASH: - self.context.logger.error( - f"Expected performative {IpfsMessage.Performative.IPFS_HASH} but got {ipfs_message.performative}." - ) - return None - ipfs_hash = ipfs_message.ipfs_hash - self.context.logger.info( - f"Successfully stored {filename} to IPFS with hash: {ipfs_hash}" - ) - return ipfs_hash - except IPFSInteractionError as e: # pragma: no cover - self.context.logger.error( - f"An error occurred while trying to send a file to IPFS: {str(e)}" - ) - return None - - def get_from_ipfs( # pylint: disable=too-many-arguments - self, - ipfs_hash: str, - filetype: Optional[SupportedFiletype] = None, - custom_loader: CustomLoaderType = None, - timeout: Optional[float] = None, - ) -> Generator[None, None, Optional[SupportedObjectType]]: - """ - Gets an object from IPFS. - - :param ipfs_hash: the ipfs hash of the file/dir to download. - :param filetype: the file type of the object being downloaded. - :param custom_loader: a custom deserializer for the object received from IPFS. - :param timeout: timeout for the request. - :returns: the downloaded object, corresponding to ipfs_hash. - """ - try: - message, dialogue = self._build_ipfs_get_file_req(ipfs_hash, timeout) - ipfs_message = yield from self._do_ipfs_request(dialogue, message, timeout) - if ipfs_message.performative != IpfsMessage.Performative.FILES: - self.context.logger.error( - f"Expected performative {IpfsMessage.Performative.FILES} but got {ipfs_message.performative}." - ) - return None - serialized_objects = ipfs_message.files - deserialized_objects = self._deserialize_ipfs_objects( - serialized_objects, filetype, custom_loader - ) - self.context.logger.info( - f"Retrieved {len(ipfs_message.files)} objects from ipfs." - ) - return deserialized_objects - except IPFSInteractionError as e: - self.context.logger.error( - f"An error occurred while trying to fetch a file from IPFS: {str(e)}" - ) - return None - - def _do_ipfs_request( - self, - dialogue: IpfsDialogue, - message: IpfsMessage, - timeout: Optional[float] = None, - ) -> Generator[None, None, IpfsMessage]: - """Performs an IPFS request, and asynchronosuly waits for response.""" - self.context.outbox.put_message(message=message) - request_nonce = self._get_request_nonce_from_dialogue(dialogue) - cast(Requests, self.context.requests).request_id_to_callback[ - request_nonce - ] = self.get_callback_request() - # notify caller by propagating potential timeout exception. - response = yield from self.wait_for_message(timeout=timeout) - ipfs_message = cast(IpfsMessage, response) - return ipfs_message - - -class TmManager(BaseBehaviour): - """Util class to be used for managing the tendermint node.""" - - _active_generator: Optional[Generator] = None - _hard_reset_sleep = 20.0 # 20s - _max_reset_retry = 5 - - # TODO: TmManager is not a BaseBehaviour. It should be - # redesigned! - matching_round = Type[AbstractRound] - - def __init__(self, **kwargs: Any): - """Initialize the `TmManager`.""" - super().__init__(**kwargs) - # whether the initiation of a tm fix has been logged - self.informed: bool = False - self.acn_communication_attempted: bool = False - - def async_act(self) -> Generator: - """The behaviour act.""" - self.context.logger.error( - f"{type(self).__name__}'s async_act was called. " - f"This is not allowed as this class is not a behaviour. " - f"Exiting the agent." - ) - error_code = 1 - yield - sys.exit(error_code) - - @property - def is_acting(self) -> bool: - """This method returns whether there is an active fix being applied.""" - return self._active_generator is not None - - @property - def hard_reset_sleep(self) -> float: - """ - Amount of time to sleep before and after performing a hard reset. - - We don't need to wait for half the reset pause duration, like in normal cases where we perform a hard reset. - - :returns: the amount of time to sleep in seconds - """ - return self._hard_reset_sleep - - def _gentle_reset(self) -> Generator[None, None, None]: - """Perform a gentle reset of the Tendermint node.""" - self.context.logger.debug("Performing a gentle reset...") - request_message, http_dialogue = self._build_http_request_message( - "GET", - self.params.tendermint_com_url + "/gentle_reset", - ) - yield from self._do_request(request_message, http_dialogue) - - def _handle_unhealthy_tm(self) -> Generator: - """This method handles the case when the tendermint node is unhealthy.""" - if not self.informed: - self.context.logger.warning( - "The local deadline for the next `begin_block` request from the Tendermint node has expired! " - "Trying to reset local Tendermint node as there could be something wrong with the communication." - ) - self.informed = True - - if not self.gentle_reset_attempted: - self.gentle_reset_attempted = True - yield from self._gentle_reset() - yield from self._check_sync() - return - - is_multi_agent_service = self.synchronized_data.max_participants > 1 - if is_multi_agent_service: - # since we have reached this point, that means that the cause of blocks not being received - # cannot be fixed with a simple gentle reset, - # therefore, we request the recovery parameters via the ACN, and if we succeed, we use them to recover - # we do not need to request the recovery parameters if this is a single-agent service - acn_communication_success = yield from self.request_recovery_params( - should_log=not self.acn_communication_attempted - ) - if not acn_communication_success: - if not self.acn_communication_attempted: - self.context.logger.error( - "Failed to get the recovery parameters via the ACN. Cannot reset Tendermint." - ) - self.acn_communication_attempted = True - return - - recovery_params = self.shared_state.tm_recovery_params - self.round_sequence.reset_state( - restart_from_round=recovery_params.reset_from_round, - round_count=recovery_params.round_count, - serialized_db_state=recovery_params.serialized_db_state, - ) - - for _ in range(self._max_reset_retry): - reset_successfully = yield from self.reset_tendermint_with_wait( - on_startup=True, - is_recovery=True, - ) - if reset_successfully: - self.context.logger.info("Tendermint reset was successfully performed.") - # we sleep to give some time for tendermint to start sending us blocks - # otherwise we might end-up assuming that tendermint is still not working. - # Note that the wait_from_last_timestamp() in reset_tendermint_with_wait() - # doesn't guarantee us this, since the block stall deadline is greater than the - # hard_reset_sleep, 60s vs 20s. In other words, we haven't received a block for at - # least 60s, so wait_from_last_timestamp() will return immediately. - # By setting "on_startup" to True in the reset_tendermint_with_wait() call above, - # wait_from_last_timestamp() will not be called at all. - yield from self.sleep(self.hard_reset_sleep) - self.gentle_reset_attempted = False - return - - self.context.logger.error("Failed to reset tendermint.") - - def _get_reset_params(self, default: bool) -> Optional[Dict[str, str]]: - """ - Get the parameters for a hard reset request when trying to recover agent <-> tendermint communication. - - :param default: ignored for this use case. - :returns: the reset params. - """ - # we get the params from the latest successful reset, if they are not available, - # i.e. no successful reset has been performed, we return None. - # Returning None means default params will be used. - return self.shared_state.tm_recovery_params.reset_params - - def get_callback_request(self) -> Callable[[Message, "BaseBehaviour"], None]: - """Wrapper for callback_request(), overridden to remove checks not applicable to TmManager.""" - - def callback_request( - message: Message, _current_behaviour: BaseBehaviour - ) -> None: - """ - This method gets called when a response for a prior request is received. - - Overridden to remove the check that checks whether the behaviour that made the request is still active. - The received message gets passed to the behaviour that invoked it, in this case it's always the TmManager. - - :param message: the response. - :param _current_behaviour: not used, left in to satisfy the interface. - :return: none - """ - if self.state == AsyncBehaviour.AsyncState.WAITING_MESSAGE: - self.try_send(message) - else: - self.context.logger.warning( - "Could not send message to TmManager: %s", message - ) - - return callback_request - - def try_fix(self) -> None: - """This method tries to fix an unhealthy tendermint node.""" - if self._active_generator is None: - # There is no active generator set, we need to create one. - # A generator being active means that a reset operation is - # being performed. - self._active_generator = self._handle_unhealthy_tm() - try: - # if the behaviour is waiting for a message - # we check whether one has arrived, and if it has - # we send it to the generator. - if self.state == self.AsyncState.WAITING_MESSAGE: - if self.is_notified: - self._active_generator.send(self.received_message) - self._on_sent_message() - # note that if the behaviour is waiting for - # a message, we deliberately don't send a tick - # this was done to have consistency between - # the act here, and acts on normal AsyncBehaviours - return - # this will run the active generator until - # the first yield statement is encountered - self._active_generator.send(None) - - except StopIteration: - # the generator is finished - self.context.logger.debug("Applying tendermint fix finished.") - self._active_generator = None - # the following is required because the message - # 'tick' might be the last one the generator needs - # to complete. In that scenario, we need to call - # the callback here - if self.is_notified: - self._on_sent_message() - - -class DegenerateBehaviour(BaseBehaviour, ABC): - """An abstract matching behaviour for final and degenerate rounds.""" - - matching_round: Type[AbstractRound] - is_degenerate: bool = True - sleep_time_before_exit = 5.0 - - def async_act(self) -> Generator: - """Exit the agent with error when a degenerate round is reached.""" - self.context.logger.error( - "The execution reached a degenerate behaviour. " - "This means a degenerate round has been reached during " - "the execution of the ABCI application. Please check the " - "functioning of the ABCI app." - ) - self.context.logger.error( - f"Sleeping {self.sleep_time_before_exit} seconds before exiting." - ) - yield from self.sleep(self.sleep_time_before_exit) - error_code = 1 - sys.exit(error_code) - - -def make_degenerate_behaviour( - round_cls: Type[AbstractRound], -) -> Type[DegenerateBehaviour]: - """Make a degenerate behaviour class.""" - - class NewDegenerateBehaviour(DegenerateBehaviour): - """A newly defined degenerate behaviour class.""" - - matching_round = round_cls - - new_behaviour_cls = NewDegenerateBehaviour - new_behaviour_cls.__name__ = f"DegenerateBehaviour_{round_cls.auto_round_id()}" # pylint: disable=attribute-defined-outside-init - return new_behaviour_cls diff --git a/packages/valory/skills/abstract_round_abci/behaviours.py b/packages/valory/skills/abstract_round_abci/behaviours.py deleted file mode 100644 index fcf69ea..0000000 --- a/packages/valory/skills/abstract_round_abci/behaviours.py +++ /dev/null @@ -1,409 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the behaviours for the 'abstract_round_abci' skill.""" - -from abc import ABC, ABCMeta -from collections import defaultdict -from dataclasses import asdict -from typing import ( - AbstractSet, - Any, - Dict, - Generator, - Generic, - List, - Optional, - Set, - Tuple, - Type, - cast, -) - -from aea.skills.base import Behaviour - -from packages.valory.skills.abstract_round_abci.base import ( - ABCIAppInternalError, - AbciApp, - AbstractRound, - EventType, - PendingOffencesPayload, - PendingOffencesRound, - PendingOffense, - RoundSequence, -) -from packages.valory.skills.abstract_round_abci.behaviour_utils import ( - BaseBehaviour, - TmManager, - make_degenerate_behaviour, -) -from packages.valory.skills.abstract_round_abci.models import SharedState - - -SLASHING_BACKGROUND_BEHAVIOUR_ID = "slashing_check_behaviour" -TERMINATION_BACKGROUND_BEHAVIOUR_ID = "background_behaviour" - - -BehaviourType = Type[BaseBehaviour] -Action = Optional[str] -TransitionFunction = Dict[BehaviourType, Dict[Action, BehaviourType]] - - -class _MetaRoundBehaviour(ABCMeta): - """A metaclass that validates AbstractRoundBehaviour's attributes.""" - - are_background_behaviours_set: bool = False - - def __new__(mcs, name: str, bases: Tuple, namespace: Dict, **kwargs: Any) -> Type: # type: ignore - """Initialize the class.""" - new_cls = super().__new__(mcs, name, bases, namespace, **kwargs) - - if ABC in bases: - # abstract class, return - return new_cls - if not issubclass(new_cls, AbstractRoundBehaviour): - # the check only applies to AbstractRoundBehaviour subclasses - return new_cls - - mcs.are_background_behaviours_set = bool( - new_cls.background_behaviours_cls - {PendingOffencesBehaviour} - ) - mcs._check_consistency(cast(AbstractRoundBehaviour, new_cls)) - return new_cls - - @classmethod - def _check_consistency(mcs, behaviour_cls: "AbstractRoundBehaviour") -> None: - """Check consistency of class attributes.""" - mcs._check_all_required_classattributes_are_set(behaviour_cls) - mcs._check_behaviour_id_uniqueness(behaviour_cls) - mcs._check_initial_behaviour_in_set_of_behaviours(behaviour_cls) - mcs._check_matching_round_consistency(behaviour_cls) - - @classmethod - def _check_all_required_classattributes_are_set( - mcs, behaviour_cls: "AbstractRoundBehaviour" - ) -> None: - """Check that all the required class attributes are set.""" - try: - _ = behaviour_cls.abci_app_cls - _ = behaviour_cls.behaviours - _ = behaviour_cls.initial_behaviour_cls - except AttributeError as e: - raise ABCIAppInternalError(*e.args) from None - - @classmethod - def _check_behaviour_id_uniqueness( - mcs, behaviour_cls: "AbstractRoundBehaviour" - ) -> None: - """Check that behaviour ids are unique across behaviours.""" - behaviour_id_to_behaviour = defaultdict(lambda: []) - for behaviour_class in behaviour_cls.behaviours: - behaviour_id_to_behaviour[behaviour_class.auto_behaviour_id()].append( - behaviour_class - ) - if len(behaviour_id_to_behaviour[behaviour_class.auto_behaviour_id()]) > 1: - behaviour_classes_names = [ - _behaviour_cls.__name__ - for _behaviour_cls in behaviour_id_to_behaviour[ - behaviour_class.auto_behaviour_id() - ] - ] - raise ABCIAppInternalError( - f"behaviours {behaviour_classes_names} have the same behaviour id '{behaviour_class.auto_behaviour_id()}'" - ) - - @classmethod - def _check_matching_round_consistency( - mcs, behaviour_cls: "AbstractRoundBehaviour" - ) -> None: - """Check that matching rounds are: (1) unique across behaviour, and (2) covering.""" - matching_bg_round_classes = { - behaviour_cls.matching_round - for behaviour_cls in behaviour_cls.background_behaviours_cls - } - round_to_behaviour: Dict[Type[AbstractRound], List[BehaviourType]] = { - round_cls: [] - for round_cls in behaviour_cls.abci_app_cls.get_all_round_classes( - matching_bg_round_classes, - mcs.are_background_behaviours_set, - ) - } - - # check uniqueness - for b in behaviour_cls.behaviours: - behaviours = round_to_behaviour.get(b.matching_round, None) - if behaviours is None: - raise ABCIAppInternalError( - f"Behaviour {b.behaviour_id!r} specifies unknown {b.matching_round!r} as a matching round. " - "Please make sure that the round is implemented and belongs to the FSM. " - f"If {b.behaviour_id!r} is a background behaviour, please make sure that it is set correctly, " - f"by overriding the corresponding attribute of the chained skill's behaviour." - ) - behaviours.append(b) - if len(behaviours) > 1: - behaviour_cls_ids = [ - behaviour_cls_.auto_behaviour_id() for behaviour_cls_ in behaviours - ] - raise ABCIAppInternalError( - f"behaviours {behaviour_cls_ids} have the same matching round '{b.matching_round.auto_round_id()}'" - ) - - # check covering - for round_cls, behaviours in round_to_behaviour.items(): - if round_cls in behaviour_cls.abci_app_cls.final_states: - if len(behaviours) != 0: - raise ABCIAppInternalError( - f"round {round_cls.auto_round_id()} is a final round it shouldn't have any matching behaviours." - ) - elif len(behaviours) == 0: - raise ABCIAppInternalError( - f"round {round_cls.auto_round_id()} is not a matching round of any behaviour" - ) - - @classmethod - def _check_initial_behaviour_in_set_of_behaviours( - mcs, behaviour_cls: "AbstractRoundBehaviour" - ) -> None: - """Check the initial behaviour is in the set of behaviours.""" - if behaviour_cls.initial_behaviour_cls not in behaviour_cls.behaviours: - raise ABCIAppInternalError( - f"initial behaviour {behaviour_cls.initial_behaviour_cls.auto_behaviour_id()} is not in the set of behaviours" - ) - - -class PendingOffencesBehaviour(BaseBehaviour): - """A behaviour responsible for checking whether there are any pending offences.""" - - matching_round = PendingOffencesRound - - @property - def round_sequence(self) -> RoundSequence: - """Get the round sequence from the shared state.""" - return cast(SharedState, self.context.state).round_sequence - - @property - def pending_offences(self) -> Set[PendingOffense]: - """Get the pending offences from the round sequence.""" - return self.round_sequence.pending_offences - - def has_pending_offences(self) -> bool: - """Check if there are any pending offences.""" - return bool(len(self.pending_offences)) - - def async_act(self) -> Generator: - """ - Checks the pending offences. - - This behaviour simply checks if the set of pending offences is not empty. - When it’s not empty, it pops the offence from the set, and sends it to the rest of the agents via a payload - - :return: None - :yield: None - """ - yield from self.wait_for_condition(self.has_pending_offences) - offence = self.pending_offences.pop() - offence_detected_log = ( - f"An offence of type {offence.offense_type.name} has been detected " - f"for agent with address {offence.accused_agent_address} during round {offence.round_count}. " - ) - offence_expiration = offence.last_transition_timestamp + offence.time_to_live - last_timestamp = self.round_sequence.last_round_transition_timestamp - - if offence_expiration < last_timestamp.timestamp(): - ignored_log = "Offence will be ignored as it has expired." - self.context.logger.info(offence_detected_log + ignored_log) - return - - sharing_log = "Sharing offence with the other agents." - self.context.logger.info(offence_detected_log + sharing_log) - - payload = PendingOffencesPayload( - self.context.agent_address, *asdict(offence).values() - ) - yield from self.send_a2a_transaction(payload) - yield from self.wait_until_round_end() - self.set_done() - - -class AbstractRoundBehaviour( # pylint: disable=too-many-instance-attributes - Behaviour, ABC, Generic[EventType], metaclass=_MetaRoundBehaviour -): - """This behaviour implements an abstract round behaviour.""" - - abci_app_cls: Type[AbciApp[EventType]] - behaviours: AbstractSet[BehaviourType] - initial_behaviour_cls: BehaviourType - background_behaviours_cls: Set[BehaviourType] = {PendingOffencesBehaviour} # type: ignore - - def __init__(self, **kwargs: Any) -> None: - """Initialize the behaviour.""" - super().__init__(**kwargs) - self._behaviour_id_to_behaviours: Dict[ - str, BehaviourType - ] = self._get_behaviour_id_to_behaviour_mapping(self.behaviours) - self._round_to_behaviour: Dict[ - Type[AbstractRound], BehaviourType - ] = self._get_round_to_behaviour_mapping(self.behaviours) - - self.current_behaviour: Optional[BaseBehaviour] = None - self.background_behaviours: Set[BaseBehaviour] = set() - self.tm_manager: Optional[TmManager] = None - # keep track of last round height so to detect changes - self._last_round_height = 0 - - # this variable remembers the actual next transition - # when we cannot preemptively interrupt the current behaviour - # because it has not a matching round. - self._next_behaviour_cls: Optional[BehaviourType] = None - - @classmethod - def _get_behaviour_id_to_behaviour_mapping( - cls, behaviours: AbstractSet[BehaviourType] - ) -> Dict[str, BehaviourType]: - """Get behaviour id to behaviour mapping.""" - result: Dict[str, BehaviourType] = {} - for behaviour_cls in behaviours: - behaviour_id = behaviour_cls.auto_behaviour_id() - if behaviour_id in result: - raise ValueError( - f"cannot have two behaviours with the same id; got {behaviour_cls} and {result[behaviour_id]} both with id '{behaviour_id}'" - ) - result[behaviour_id] = behaviour_cls - return result - - @classmethod - def _get_round_to_behaviour_mapping( - cls, behaviours: AbstractSet[BehaviourType] - ) -> Dict[Type[AbstractRound], BehaviourType]: - """Get round-to-behaviour mapping.""" - result: Dict[Type[AbstractRound], BehaviourType] = {} - for behaviour_cls in behaviours: - round_cls = behaviour_cls.matching_round - if round_cls in result: - raise ValueError( - f"the behaviours '{behaviour_cls.auto_behaviour_id()}' and '{result[round_cls].auto_behaviour_id()}' point to the same matching round '{round_cls.auto_round_id()}'" - ) - result[round_cls] = behaviour_cls - - # iterate over rounds and map final (i.e. degenerate) rounds - # to the degenerate behaviour class - for final_round_cls in cls.abci_app_cls.final_states: - new_degenerate_behaviour = make_degenerate_behaviour(final_round_cls) - new_degenerate_behaviour.matching_round = final_round_cls - result[final_round_cls] = new_degenerate_behaviour - - return result - - def instantiate_behaviour_cls(self, behaviour_cls: BehaviourType) -> BaseBehaviour: - """Instantiate the behaviours class.""" - return behaviour_cls( - name=behaviour_cls.auto_behaviour_id(), skill_context=self.context - ) - - def _setup_background(self) -> None: - """Set up the background behaviours.""" - params = cast(BaseBehaviour, self.current_behaviour).params - for background_cls in self.background_behaviours_cls: - background_cls = cast(Type[BaseBehaviour], background_cls) - - if ( - not params.use_termination - and background_cls.auto_behaviour_id() - == TERMINATION_BACKGROUND_BEHAVIOUR_ID - ) or ( - not params.use_slashing - and background_cls.auto_behaviour_id() - == SLASHING_BACKGROUND_BEHAVIOUR_ID - or background_cls == PendingOffencesBehaviour - ): - # comparing with the behaviour id is not entirely safe, as there is a potential for conflicts - # if a user creates a behaviour with the same name - continue - - self.background_behaviours.add( - self.instantiate_behaviour_cls(background_cls) - ) - - def setup(self) -> None: - """Set up the behaviours.""" - self.current_behaviour = self.instantiate_behaviour_cls( - self.initial_behaviour_cls - ) - self.tm_manager = self.instantiate_behaviour_cls(TmManager) # type: ignore - self._setup_background() - - def teardown(self) -> None: - """Tear down the behaviour""" - - def _background_act(self) -> None: - """Call the act wrapper for the background behaviours.""" - for behaviour in self.background_behaviours: - behaviour.act_wrapper() - - def act(self) -> None: - """Implement the behaviour.""" - tm_manager = cast(TmManager, self.tm_manager) - if tm_manager.tm_communication_unhealthy or tm_manager.is_acting: - # tendermint is not healthy, or we are already applying a fix. - # try_fix() internally uses generators, that's why it's relevant - # to know whether a fix is already being applied. - # It might happen that tendermint is healthy, but the fix is not yet finished. - tm_manager.try_fix() - return - - tm_manager.informed = False - tm_manager.acn_communication_attempted = False - self._process_current_round() - if self.current_behaviour is None: - return - - self.current_behaviour.act_wrapper() - if self.current_behaviour.is_done(): - self.current_behaviour.clean_up() - self.current_behaviour = None - - self._background_act() - - def _process_current_round(self) -> None: - """Process current ABCIApp round.""" - current_round_height = self.context.state.round_sequence.current_round_height - if ( - self.current_behaviour is not None - and self._last_round_height == current_round_height - ): - # round has not changed - do nothing - return - self._last_round_height = current_round_height - current_round_cls = type(self.context.state.round_sequence.current_round) - - # each round has a behaviour associated to it - next_behaviour_cls = self._round_to_behaviour[current_round_cls] - - # stop the current behaviour and replace it with the new behaviour - if self.current_behaviour is not None: - current_behaviour = cast(BaseBehaviour, self.current_behaviour) - current_behaviour.clean_up() - current_behaviour.stop() - self.context.logger.debug( - "overriding transition: current behaviour: '%s', next behaviour: '%s'", - self.current_behaviour.behaviour_id if self.current_behaviour else None, - next_behaviour_cls.auto_behaviour_id(), - ) - - self.current_behaviour = self.instantiate_behaviour_cls(next_behaviour_cls) diff --git a/packages/valory/skills/abstract_round_abci/common.py b/packages/valory/skills/abstract_round_abci/common.py deleted file mode 100644 index c6ee52e..0000000 --- a/packages/valory/skills/abstract_round_abci/common.py +++ /dev/null @@ -1,231 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the behaviours, round and payloads for the 'abstract_round_abci' skill.""" - -import hashlib -import random -from abc import ABC -from math import floor -from typing import Any, Dict, Generator, List, Optional, Type, Union, cast - -from packages.valory.protocols.ledger_api.message import LedgerApiMessage -from packages.valory.skills.abstract_round_abci.base import BaseTxPayload -from packages.valory.skills.abstract_round_abci.behaviour_utils import BaseBehaviour -from packages.valory.skills.abstract_round_abci.utils import VerifyDrand - - -RandomnessObservation = Optional[Dict[str, Union[str, int]]] - - -drand_check = VerifyDrand() - - -def random_selection(elements: List[Any], randomness: float) -> str: - """ - Select a random element from a list. - - :param: elements: a list of elements to choose among - :param: randomness: a random number in the [0,1) interval - :return: a randomly chosen element - """ - if not elements: - raise ValueError("No elements to randomly select among") - if randomness < 0 or randomness >= 1: - raise ValueError("Randomness should lie in the [0,1) interval") - random_position = floor(randomness * len(elements)) - return elements[random_position] - - -class RandomnessBehaviour(BaseBehaviour, ABC): - """Behaviour to collect randomness values from DRAND service for keeper agent selection.""" - - payload_class: Type[BaseTxPayload] - - def failsafe_randomness( - self, - ) -> Generator[None, None, RandomnessObservation]: - """ - This methods provides a failsafe for randomness retrieval. - - :return: derived randomness - :yields: derived randomness - """ - ledger_api_response = yield from self.get_ledger_api_response( - performative=LedgerApiMessage.Performative.GET_STATE, # type: ignore - ledger_callable="get_block", - block_identifier="latest", - ) - - if ( - ledger_api_response.performative == LedgerApiMessage.Performative.ERROR - or "hash" not in ledger_api_response.state.body - ): - return None - - randomness = hashlib.sha256( - cast(str, ledger_api_response.state.body.get("hash")).encode() - + str(self.params.service_id).encode() - ).hexdigest() - return {"randomness": randomness, "round": 0} - - def get_randomness_from_api( - self, - ) -> Generator[None, None, RandomnessObservation]: - """Retrieve randomness from given api specs.""" - api_specs = self.context.randomness_api.get_spec() - response = yield from self.get_http_response( - method=api_specs["method"], - url=api_specs["url"], - ) - observation = self.context.randomness_api.process_response(response) - if observation is not None: - self.context.logger.info("Verifying DRAND values...") - check, error = drand_check.verify(observation, self.params.drand_public_key) - if check: - self.context.logger.info("DRAND check successful.") - else: - self.context.logger.error(f"DRAND check failed, {error}.") - return None - return observation - - def async_act(self) -> Generator: - """ - Retrieve randomness from API. - - Steps: - - Do a http request to the API. - - Retry until receiving valid values for randomness or retries exceed. - - If retrieved values are valid continue else generate randomness from chain. - """ - with self.context.benchmark_tool.measure(self.behaviour_id).local(): - if self.context.randomness_api.is_retries_exceeded(): - self.context.logger.warning("Cannot retrieve randomness from api.") - self.context.logger.info("Generating randomness from chain...") - observation = yield from self.failsafe_randomness() - if observation is None: - self.context.logger.error( - "Could not generate randomness from chain!" - ) - return - else: - self.context.logger.info("Retrieving DRAND values from api...") - observation = yield from self.get_randomness_from_api() - self.context.logger.info(f"Retrieved DRAND values: {observation}.") - - if observation: - payload = self.payload_class( # type: ignore - self.context.agent_address, - round_id=observation["round"], - randomness=observation["randomness"], - ) - with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): - yield from self.send_a2a_transaction(payload) - yield from self.wait_until_round_end() - - self.set_done() - else: - self.context.logger.error( - f"Could not get randomness from {self.context.randomness_api.api_id}" - ) - yield from self.sleep( - self.context.randomness_api.retries_info.suggested_sleep_time - ) - self.context.randomness_api.increment_retries() - - def clean_up(self) -> None: - """ - Clean up the resources due to a 'stop' event. - - It can be optionally implemented by the concrete classes. - """ - self.context.randomness_api.reset_retries() - - -class SelectKeeperBehaviour(BaseBehaviour, ABC): - """Select the keeper agent.""" - - payload_class: Type[BaseTxPayload] - - def _select_keeper(self) -> str: - """ - Select a new keeper randomly. - - 1. Sort the list of participants who are not blacklisted as keepers. - 2. Randomly shuffle it. - 3. Pick the first keeper in order. - 4. If he has already been selected, pick the next one. - - :return: the selected keeper's address. - """ - # Get all the participants who have not been blacklisted as keepers - non_blacklisted = ( - self.synchronized_data.participants - - self.synchronized_data.blacklisted_keepers - ) - if not non_blacklisted: - raise RuntimeError( - "Cannot continue if all the keepers have been blacklisted!" - ) - - # Sorted list of participants who are not blacklisted as keepers - relevant_set = sorted(list(non_blacklisted)) - - # Random seeding and shuffling of the set - random.seed(self.synchronized_data.keeper_randomness) - random.shuffle(relevant_set) - - # If the keeper is not set yet, pick the first address - keeper_address = relevant_set[0] - - # If the keeper has been already set, select the next. - if ( - self.synchronized_data.is_keeper_set - and len(self.synchronized_data.participants) > 1 - ): - old_keeper_index = relevant_set.index( - self.synchronized_data.most_voted_keeper_address - ) - keeper_address = relevant_set[(old_keeper_index + 1) % len(relevant_set)] - - self.context.logger.info(f"Selected a new keeper: {keeper_address}.") - - return keeper_address - - def async_act(self) -> Generator: - """ - Do the action. - - Steps: - - Select a keeper randomly. - - Send the transaction with the keeper and wait for it to be mined. - - Wait until ABCI application transitions to the next round. - - Go to the next behaviour state (set done event). - """ - - with self.context.benchmark_tool.measure(self.behaviour_id).local(): - payload = self.payload_class( # type: ignore - self.context.agent_address, self._select_keeper() - ) - - with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): - yield from self.send_a2a_transaction(payload) - yield from self.wait_until_round_end() - - self.set_done() diff --git a/packages/valory/skills/abstract_round_abci/dialogues.py b/packages/valory/skills/abstract_round_abci/dialogues.py deleted file mode 100644 index 7f0b53a..0000000 --- a/packages/valory/skills/abstract_round_abci/dialogues.py +++ /dev/null @@ -1,368 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the classes required for dialogue management.""" - -from typing import Any, Optional, Type - -from aea.exceptions import enforce -from aea.helpers.transaction.base import Terms -from aea.protocols.base import Address, Message -from aea.protocols.dialogue.base import Dialogue as BaseDialogue -from aea.protocols.dialogue.base import DialogueLabel as BaseDialogueLabel -from aea.skills.base import Model - -from packages.open_aea.protocols.signing.dialogues import ( - SigningDialogue as BaseSigningDialogue, -) -from packages.open_aea.protocols.signing.dialogues import ( - SigningDialogues as BaseSigningDialogues, -) -from packages.valory.protocols.abci.dialogues import AbciDialogue as BaseAbciDialogue -from packages.valory.protocols.abci.dialogues import AbciDialogues as BaseAbciDialogues -from packages.valory.protocols.contract_api import ContractApiMessage -from packages.valory.protocols.contract_api.dialogues import ( - ContractApiDialogue as BaseContractApiDialogue, -) -from packages.valory.protocols.contract_api.dialogues import ( - ContractApiDialogues as BaseContractApiDialogues, -) -from packages.valory.protocols.http.dialogues import HttpDialogue as BaseHttpDialogue -from packages.valory.protocols.http.dialogues import HttpDialogues as BaseHttpDialogues -from packages.valory.protocols.ipfs.dialogues import IpfsDialogue as BaseIpfsDialogue -from packages.valory.protocols.ipfs.dialogues import IpfsDialogues as BaseIpfsDialogues -from packages.valory.protocols.ledger_api import LedgerApiMessage -from packages.valory.protocols.ledger_api.dialogues import ( - LedgerApiDialogue as BaseLedgerApiDialogue, -) -from packages.valory.protocols.ledger_api.dialogues import ( - LedgerApiDialogues as BaseLedgerApiDialogues, -) -from packages.valory.protocols.tendermint.dialogues import ( - TendermintDialogue as BaseTendermintDialogue, -) -from packages.valory.protocols.tendermint.dialogues import ( - TendermintDialogues as BaseTendermintDialogues, -) - - -AbciDialogue = BaseAbciDialogue - - -class AbciDialogues(Model, BaseAbciDialogues): - """The dialogues class keeps track of all dialogues.""" - - def __init__(self, **kwargs: Any) -> None: - """ - Initialize dialogues. - - :param kwargs: keyword arguments - """ - Model.__init__(self, **kwargs) - - def role_from_first_message( # pylint: disable=unused-argument - message: Message, receiver_address: Address - ) -> BaseDialogue.Role: - """Infer the role of the agent from an incoming/outgoing first message - - :param message: an incoming/outgoing first message - :param receiver_address: the address of the receiving agent - :return: The role of the agent - """ - return AbciDialogue.Role.CLIENT - - BaseAbciDialogues.__init__( - self, - self_address=str(self.skill_id), - role_from_first_message=role_from_first_message, - ) - - -HttpDialogue = BaseHttpDialogue - - -class HttpDialogues(Model, BaseHttpDialogues): - """This class keeps track of all http dialogues.""" - - def __init__(self, **kwargs: Any) -> None: - """ - Initialize dialogues. - - :param kwargs: keyword arguments - """ - Model.__init__(self, **kwargs) - - def role_from_first_message( # pylint: disable=unused-argument - message: Message, receiver_address: Address - ) -> BaseDialogue.Role: - """Infer the role of the agent from an incoming/outgoing first message - - :param message: an incoming/outgoing first message - :param receiver_address: the address of the receiving agent - :return: The role of the agent - """ - return BaseHttpDialogue.Role.CLIENT - - BaseHttpDialogues.__init__( - self, - self_address=str(self.skill_id), - role_from_first_message=role_from_first_message, - ) - - -SigningDialogue = BaseSigningDialogue - - -class SigningDialogues(Model, BaseSigningDialogues): - """This class keeps track of all signing dialogues.""" - - def __init__(self, **kwargs: Any) -> None: - """ - Initialize dialogues. - - :param kwargs: keyword arguments - """ - Model.__init__(self, **kwargs) - - def role_from_first_message( # pylint: disable=unused-argument - message: Message, receiver_address: Address - ) -> BaseDialogue.Role: - """Infer the role of the agent from an incoming/outgoing first message - - :param message: an incoming/outgoing first message - :param receiver_address: the address of the receiving agent - :return: The role of the agent - """ - return BaseSigningDialogue.Role.SKILL - - BaseSigningDialogues.__init__( - self, - self_address=str(self.skill_id), - role_from_first_message=role_from_first_message, - ) - - -class LedgerApiDialogue( # pylint: disable=too-few-public-methods - BaseLedgerApiDialogue -): - """The dialogue class maintains state of a dialogue and manages it.""" - - __slots__ = ("_terms",) - - def __init__( - self, - dialogue_label: BaseDialogueLabel, - self_address: Address, - role: BaseDialogue.Role, - message_class: Type[LedgerApiMessage] = LedgerApiMessage, - ) -> None: - """ - Initialize a dialogue. - - :param dialogue_label: the identifier of the dialogue - :param self_address: the address of the entity for whom this dialogue is maintained - :param role: the role of the agent this dialogue is maintained for - :param message_class: the message class - """ - BaseLedgerApiDialogue.__init__( - self, - dialogue_label=dialogue_label, - self_address=self_address, - role=role, - message_class=message_class, - ) - self._terms = None # type: Optional[Terms] - - @property - def terms(self) -> Terms: - """Get the terms.""" - if self._terms is None: - raise ValueError("Terms not set!") - return self._terms - - @terms.setter - def terms(self, terms: Terms) -> None: - """Set the terms.""" - enforce(self._terms is None, "Terms already set!") - self._terms = terms - - -class LedgerApiDialogues(Model, BaseLedgerApiDialogues): - """The dialogues class keeps track of all dialogues.""" - - def __init__(self, **kwargs: Any) -> None: - """ - Initialize dialogues. - - :param kwargs: keyword arguments - """ - Model.__init__(self, **kwargs) - - def role_from_first_message( # pylint: disable=unused-argument - message: Message, receiver_address: Address - ) -> BaseDialogue.Role: - """Infer the role of the agent from an incoming/outgoing first message - - :param message: an incoming/outgoing first message - :param receiver_address: the address of the receiving agent - :return: The role of the agent - """ - return BaseLedgerApiDialogue.Role.AGENT - - BaseLedgerApiDialogues.__init__( - self, - self_address=str(self.skill_id), - role_from_first_message=role_from_first_message, - dialogue_class=LedgerApiDialogue, - ) - - -class ContractApiDialogue( # pylint: disable=too-few-public-methods - BaseContractApiDialogue -): - """The dialogue class maintains state of a dialogue and manages it.""" - - __slots__ = ("_terms",) - - def __init__( - self, - dialogue_label: BaseDialogueLabel, - self_address: Address, - role: BaseDialogue.Role, - message_class: Type[ContractApiMessage] = ContractApiMessage, - ) -> None: - """ - Initialize a dialogue. - - :param dialogue_label: the identifier of the dialogue - :param self_address: the address of the entity for whom this dialogue is maintained - :param role: the role of the agent this dialogue is maintained for - :param message_class: the message class - """ - BaseContractApiDialogue.__init__( - self, - dialogue_label=dialogue_label, - self_address=self_address, - role=role, - message_class=message_class, - ) - self._terms = None # type: Optional[Terms] - - @property - def terms(self) -> Terms: - """Get the terms.""" - if self._terms is None: - raise ValueError("Terms not set!") - return self._terms - - @terms.setter - def terms(self, terms: Terms) -> None: - """Set the terms.""" - enforce(self._terms is None, "Terms already set!") - self._terms = terms - - -class ContractApiDialogues(Model, BaseContractApiDialogues): - """The dialogues class keeps track of all dialogues.""" - - def __init__(self, **kwargs: Any) -> None: - """Initialize dialogues.""" - Model.__init__(self, **kwargs) - - def role_from_first_message( # pylint: disable=unused-argument - message: Message, receiver_address: Address - ) -> BaseDialogue.Role: - """Infer the role of the agent from an incoming/outgoing first message - - :param message: an incoming/outgoing first message - :param receiver_address: the address of the receiving agent - :return: The role of the agent - """ - return ContractApiDialogue.Role.AGENT - - BaseContractApiDialogues.__init__( - self, - self_address=str(self.skill_id), - role_from_first_message=role_from_first_message, - dialogue_class=ContractApiDialogue, - ) - - -TendermintDialogue = BaseTendermintDialogue - - -class TendermintDialogues(Model, BaseTendermintDialogues): - """The dialogues class keeps track of all dialogues.""" - - def __init__(self, **kwargs: Any) -> None: - """ - Initialize dialogues. - - :param kwargs: keyword arguments - """ - Model.__init__(self, **kwargs) - - def role_from_first_message( # pylint: disable=unused-argument - message: Message, receiver_address: Address - ) -> BaseDialogue.Role: - """Infer the role of the agent from an incoming/outgoing first message - - :param message: an incoming/outgoing first message - :param receiver_address: the address of the receiving agent - :return: The role of the agent - """ - return TendermintDialogue.Role.AGENT - - BaseTendermintDialogues.__init__( - self, - self_address=self.context.agent_address, - role_from_first_message=role_from_first_message, - ) - - -IpfsDialogue = BaseIpfsDialogue - - -class IpfsDialogues(Model, BaseIpfsDialogues): - """A class to keep track of IPFS dialogues.""" - - def __init__(self, **kwargs: Any) -> None: - """ - Initialize dialogues. - - :param kwargs: keyword arguments - """ - Model.__init__(self, **kwargs) - - def role_from_first_message( # pylint: disable=unused-argument - message: Message, receiver_address: Address - ) -> BaseDialogue.Role: - """Infer the role of the agent from an incoming/outgoing first message - - :param message: an incoming/outgoing first message - :param receiver_address: the address of the receiving agent - :return: The role of the agent - """ - return IpfsDialogue.Role.SKILL - - BaseIpfsDialogues.__init__( - self, - self_address=str(self.skill_id), - role_from_first_message=role_from_first_message, - ) diff --git a/packages/valory/skills/abstract_round_abci/handlers.py b/packages/valory/skills/abstract_round_abci/handlers.py deleted file mode 100644 index 88cf072..0000000 --- a/packages/valory/skills/abstract_round_abci/handlers.py +++ /dev/null @@ -1,790 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the handler for the 'abstract_round_abci' skill.""" - -import ipaddress -import json -from abc import ABC -from calendar import timegm -from dataclasses import asdict -from enum import Enum -from typing import Any, Callable, Dict, FrozenSet, List, Optional, cast - -from aea.configurations.data_types import PublicId -from aea.protocols.base import Message -from aea.protocols.dialogue.base import Dialogue, Dialogues -from aea.skills.base import Handler - -from packages.open_aea.protocols.signing import SigningMessage -from packages.valory.protocols.abci import AbciMessage -from packages.valory.protocols.abci.custom_types import Events, ValidatorUpdates -from packages.valory.protocols.contract_api import ContractApiMessage -from packages.valory.protocols.http import HttpMessage -from packages.valory.protocols.ipfs import IpfsMessage -from packages.valory.protocols.ledger_api import LedgerApiMessage -from packages.valory.protocols.tendermint.dialogues import ( - TendermintDialogue, - TendermintDialogues, -) -from packages.valory.protocols.tendermint.message import TendermintMessage -from packages.valory.skills.abstract_abci.handlers import ABCIHandler -from packages.valory.skills.abstract_round_abci.base import ( - ABCIAppInternalError, - AddBlockError, - DEFAULT_PENDING_OFFENCE_TTL, - ERROR_CODE, - LateArrivingTransaction, - OK_CODE, - OffenseType, - PendingOffense, - SignatureNotValidError, - Transaction, - TransactionNotValidError, - TransactionTypeNotRecognizedError, -) -from packages.valory.skills.abstract_round_abci.behaviours import AbstractRoundBehaviour -from packages.valory.skills.abstract_round_abci.dialogues import AbciDialogue -from packages.valory.skills.abstract_round_abci.models import ( - Requests, - SharedState, - TendermintRecoveryParams, -) - - -def exception_to_info_msg(exception: Exception) -> str: - """Transform an exception to an info string message.""" - return f"{exception.__class__.__name__}: {str(exception)}" - - -class ABCIRoundHandler(ABCIHandler): - """ABCI handler.""" - - SUPPORTED_PROTOCOL = AbciMessage.protocol_id - - def info(self, message: AbciMessage, dialogue: AbciDialogue) -> AbciMessage: - """ - Handle the 'info' request. - - As per Tendermint spec (https://github.com/tendermint/spec/blob/038f3e025a19fed9dc96e718b9834ab1b545f136/spec/abci/abci.md#info): - - - Return information about the application state. - - Used to sync Tendermint with the application during a handshake that happens on startup. - - The returned app_version will be included in the Header of every block. - - Tendermint expects last_block_app_hash and last_block_height to be updated during Commit, ensuring that Commit is never called twice for the same block height. - - :param message: the ABCI request. - :param dialogue: the ABCI dialogue. - :return: the response. - """ - # some arbitrary information - info_data = "" - # the application software semantic version - version = "" - # the application protocol version - app_version = 0 - # latest block for which the app has called Commit - last_block_height = self.context.state.round_sequence.height - # latest result of Commit - last_block_app_hash = self.context.state.round_sequence.root_hash - reply = dialogue.reply( - performative=AbciMessage.Performative.RESPONSE_INFO, - target_message=message, - info_data=info_data, - version=version, - app_version=app_version, - last_block_height=last_block_height, - last_block_app_hash=last_block_app_hash, - ) - return cast(AbciMessage, reply) - - def init_chain(self, message: AbciMessage, dialogue: AbciDialogue) -> AbciMessage: - """ - Handle a message of REQUEST_INIT_CHAIN performative. - - As per Tendermint spec (https://github.com/tendermint/spec/blob/038f3e025a19fed9dc96e718b9834ab1b545f136/spec/abci/abci.md#initchain): - - - Called once upon genesis. - - If ResponseInitChain.Validators is empty, the initial validator set will be the RequestInitChain.Validators. - - If ResponseInitChain.Validators is not empty, it will be the initial validator set (regardless of what is in RequestInitChain.Validators). - - This allows the app to decide if it wants to accept the initial validator set proposed by tendermint (ie. in the genesis file), or if it wants to use a different one (perhaps computed based on some application specific information in the genesis file). - - :param message: the ABCI request. - :param dialogue: the ABCI dialogue. - :return: the response. - """ - # Initial validator set (optional). - validators: List = [] - # Get the root hash of the last round transition as the initial application hash. - # If no round transitions have occurred yet, `last_root_hash` returns the hash of the initial abci app's state. - # `init_chain` will be called between resets when restarting again. - app_hash = self.context.state.round_sequence.last_round_transition_root_hash - cast(SharedState, self.context.state).round_sequence.init_chain( - message.initial_height - ) - reply = dialogue.reply( - performative=AbciMessage.Performative.RESPONSE_INIT_CHAIN, - target_message=message, - validators=ValidatorUpdates(validators), - app_hash=app_hash, - ) - return cast(AbciMessage, reply) - - def begin_block(self, message: AbciMessage, dialogue: AbciDialogue) -> AbciMessage: - """Handle the 'begin_block' request.""" - cast(SharedState, self.context.state).round_sequence.begin_block( - message.header, message.byzantine_validators, message.last_commit_info - ) - return super().begin_block(message, dialogue) - - def check_tx(self, message: AbciMessage, dialogue: AbciDialogue) -> AbciMessage: - """Handle the 'check_tx' request.""" - transaction_bytes = message.tx - # check we can decode the transaction - try: - transaction = Transaction.decode(transaction_bytes) - transaction.verify(self.context.default_ledger_id) - cast(SharedState, self.context.state).round_sequence.check_is_finished() - except ( - SignatureNotValidError, - TransactionNotValidError, - TransactionTypeNotRecognizedError, - ) as exception: - self._log_exception(exception) - return self._check_tx_failed( - message, dialogue, exception_to_info_msg(exception) - ) - except LateArrivingTransaction as exception: # pragma: nocover - self.context.logger.debug(exception_to_info_msg(exception)) - return self._check_tx_failed( - message, dialogue, exception_to_info_msg(exception) - ) - - # return check_tx success - reply = dialogue.reply( - performative=AbciMessage.Performative.RESPONSE_CHECK_TX, - target_message=message, - code=OK_CODE, - data=b"", - log="", - info="check_tx succeeded", - gas_wanted=0, - gas_used=0, - events=Events([]), - codespace="", - ) - return cast(AbciMessage, reply) - - def settle_pending_offence( - self, accused_agent_address: Optional[str], invalid: bool - ) -> None: - """Add an invalid pending offence or a no-offence for the given accused agent address, if possible.""" - if accused_agent_address is None: - # only add the offence if we know and can verify the sender, - # otherwise someone could pretend to be someone else, which may lead to wrong punishments - return - - round_sequence = cast(SharedState, self.context.state).round_sequence - - try: - last_round_transition_timestamp = timegm( - round_sequence.last_round_transition_timestamp.utctimetuple() - ) - except ValueError: # pragma: no cover - # do not add an offence if no round transition has been completed yet - return - - offence_type = ( - OffenseType.INVALID_PAYLOAD if invalid else OffenseType.NO_OFFENCE - ) - pending_offense = PendingOffense( - accused_agent_address, - round_sequence.current_round_height, - offence_type, - last_round_transition_timestamp, - DEFAULT_PENDING_OFFENCE_TTL, - ) - round_sequence.add_pending_offence(pending_offense) - - def deliver_tx(self, message: AbciMessage, dialogue: AbciDialogue) -> AbciMessage: - """Handle the 'deliver_tx' request.""" - transaction_bytes = message.tx - round_sequence = cast(SharedState, self.context.state).round_sequence - payload_sender: Optional[str] = None - try: - transaction = Transaction.decode(transaction_bytes) - transaction.verify(self.context.default_ledger_id) - payload_sender = transaction.payload.sender - round_sequence.check_is_finished() - round_sequence.deliver_tx(transaction) - except ( - SignatureNotValidError, - TransactionNotValidError, - TransactionTypeNotRecognizedError, - ) as exception: - self._log_exception(exception) - # the transaction is invalid, it's potentially an offence, so we add it to the list of pending offences - self.settle_pending_offence(payload_sender, invalid=True) - return self._deliver_tx_failed( - message, dialogue, exception_to_info_msg(exception) - ) - except LateArrivingTransaction as exception: # pragma: nocover - self.context.logger.debug(exception_to_info_msg(exception)) - return self._deliver_tx_failed( - message, dialogue, exception_to_info_msg(exception) - ) - - # the invalid payloads' availability window needs to be populated with the negative values as well - self.settle_pending_offence(payload_sender, invalid=False) - - # return deliver_tx success - reply = dialogue.reply( - performative=AbciMessage.Performative.RESPONSE_DELIVER_TX, - target_message=message, - code=OK_CODE, - data=b"", - log="", - info="deliver_tx succeeded", - gas_wanted=0, - gas_used=0, - events=Events([]), - codespace="", - ) - return cast(AbciMessage, reply) - - def end_block(self, message: AbciMessage, dialogue: AbciDialogue) -> AbciMessage: - """Handle the 'end_block' request.""" - self.context.state.round_sequence.tm_height = message.height - cast(SharedState, self.context.state).round_sequence.end_block() - return super().end_block(message, dialogue) - - def commit(self, message: AbciMessage, dialogue: AbciDialogue) -> AbciMessage: - """ - Handle the 'commit' request. - - As per Tendermint spec (https://github.com/tendermint/spec/blob/038f3e025a19fed9dc96e718b9834ab1b545f136/spec/abci/abci.md#commit): - - Empty request meant to signal to the app it can write state transitions to state. - - - Persist the application state. - - Return a Merkle root hash of the application state. - - It's critical that all application instances return the same hash. If not, they will not be able to agree on the next block, because the hash is included in the next block! - - :param message: the ABCI request. - :param dialogue: the ABCI dialogue. - :return: the response. - """ - try: - cast(SharedState, self.context.state).round_sequence.commit() - except AddBlockError as exception: - self._log_exception(exception) - raise exception - # The Merkle root hash of the application state. - data = self.context.state.round_sequence.root_hash - # Blocks below this height may be removed. Defaults to 0 (retain all). - retain_height = 0 - # return commit success - reply = dialogue.reply( - performative=AbciMessage.Performative.RESPONSE_COMMIT, - target_message=message, - data=data, - retain_height=retain_height, - ) - return cast(AbciMessage, reply) - - @classmethod - def _check_tx_failed( - cls, message: AbciMessage, dialogue: AbciDialogue, info: str = "" - ) -> AbciMessage: - """Handle a failed check_tx request.""" - reply = dialogue.reply( - performative=AbciMessage.Performative.RESPONSE_CHECK_TX, - target_message=message, - code=ERROR_CODE, - data=b"", - log="", - info=info, - gas_wanted=0, - gas_used=0, - events=Events([]), - codespace="", - ) - return cast(AbciMessage, reply) - - @classmethod - def _deliver_tx_failed( - cls, message: AbciMessage, dialogue: AbciDialogue, info: str = "" - ) -> AbciMessage: - """Handle a failed deliver_tx request.""" - reply = dialogue.reply( - performative=AbciMessage.Performative.RESPONSE_DELIVER_TX, - target_message=message, - code=ERROR_CODE, - data=b"", - log="", - info=info, - gas_wanted=0, - gas_used=0, - events=Events([]), - codespace="", - ) - return cast(AbciMessage, reply) - - def _log_exception(self, exception: Exception) -> None: - """Log an exception.""" - self.context.logger.error(exception_to_info_msg(exception)) - - -class AbstractResponseHandler(Handler, ABC): - """ - Abstract response Handler. - - This abstract handler works in tandem with the 'Requests' model. - Whenever a message of 'response' type arrives, the handler - tries to dispatch it to a pending request previously registered - in 'Requests' by some other code in the same skill. - - The concrete classes must set the 'allowed_response_performatives' - class attribute to the (frozen)set of performative the developer - wants the handler to handle. - """ - - allowed_response_performatives: FrozenSet[Message.Performative] - - def setup(self) -> None: - """Set up the handler.""" - - def teardown(self) -> None: - """Tear down the handler.""" - - def handle(self, message: Message) -> None: - """ - Handle the response message. - - Steps: - 1. Try to recover the 'dialogues' instance, for the protocol - of this handler, from the skill context. The attribute name used to - read the attribute is computed by '_get_dialogues_attribute_name()' - method. If no dialogues instance is found, log a message and return. - 2. Try to recover the dialogue; if no dialogue is present, log a message - and return. - 3. Check whether the performative is in the set of allowed performative; - if not, log a message and return. - 4. Try to recover the callback of the request associated to the response - from the 'Requests' model; if no callback is present, log a message - and return. - 5. If the above check have passed, then call the callback with the - received message. - - :param message: the message to handle. - """ - protocol_dialogues = self._recover_protocol_dialogues() - if protocol_dialogues is None: - self._handle_missing_dialogues() - return - protocol_dialogues = cast(Dialogues, protocol_dialogues) - - protocol_dialogue = cast(Optional[Dialogue], protocol_dialogues.update(message)) - if protocol_dialogue is None: - self._handle_unidentified_dialogue(message) - return - - if message.performative not in self.allowed_response_performatives: - self._handle_unallowed_performative(message) - return - - request_nonce = protocol_dialogue.dialogue_label.dialogue_reference[0] - ctx_requests = cast(Requests, self.context.requests) - - try: - callback = cast( - Callable, - ctx_requests.request_id_to_callback.pop(request_nonce), - ) - except KeyError as e: - raise ABCIAppInternalError( - f"No callback defined for request with nonce: {request_nonce}" - ) from e - - self._log_message_handling(message) - current_behaviour = cast( - AbstractRoundBehaviour, self.context.behaviours.main - ).current_behaviour - callback(message, current_behaviour) - - def _get_dialogues_attribute_name(self) -> str: - """ - Get dialogues attribute name. - - By convention, the Dialogues model of the skill follows - the template '{protocol_name}_dialogues'. - - Override this method accordingly if the name of hte Dialogues - model is different. - - :return: the dialogues attribute name. - """ - return cast(PublicId, self.SUPPORTED_PROTOCOL).name + "_dialogues" - - def _recover_protocol_dialogues(self) -> Optional[Dialogues]: - """ - Recover protocol dialogues from supported protocol id. - - :return: the dialogues, or None if the dialogues object was not found. - """ - attribute = self._get_dialogues_attribute_name() - return getattr(self.context, attribute, None) - - def _handle_missing_dialogues(self) -> None: - """Handle missing dialogues in context.""" - expected_attribute_name = self._get_dialogues_attribute_name() - self.context.logger.warning( - "Cannot find Dialogues object in skill context with attribute name: %s", - expected_attribute_name, - ) - - def _handle_unidentified_dialogue(self, message: Message) -> None: - """ - Handle an unidentified dialogue. - - :param message: the unidentified message to be handled - """ - self.context.logger.warning( - "Received invalid message: unidentified dialogue. message=%s", message - ) - - def _handle_unallowed_performative(self, message: Message) -> None: - """ - Handle a message with an unallowed response performative. - - Log an error message saying that the handler did not expect requests - but only responses. - - :param message: the message - """ - self.context.logger.warning( - "Received invalid message: unallowed performative. message=%s.", message - ) - - def _log_message_handling(self, message: Message) -> None: - """Log the handling of the message.""" - self.context.logger.debug( - "Calling registered callback with message=%s", message - ) - - -class HttpHandler(AbstractResponseHandler): - """The HTTP response handler.""" - - SUPPORTED_PROTOCOL: Optional[PublicId] = HttpMessage.protocol_id - allowed_response_performatives = frozenset({HttpMessage.Performative.RESPONSE}) - - -class SigningHandler(AbstractResponseHandler): - """Implement the transaction handler.""" - - SUPPORTED_PROTOCOL: Optional[PublicId] = SigningMessage.protocol_id - allowed_response_performatives = frozenset( - { - SigningMessage.Performative.SIGNED_MESSAGE, - SigningMessage.Performative.SIGNED_TRANSACTION, - SigningMessage.Performative.ERROR, - } - ) - - -class LedgerApiHandler(AbstractResponseHandler): - """Implement the ledger handler.""" - - SUPPORTED_PROTOCOL: Optional[PublicId] = LedgerApiMessage.protocol_id - allowed_response_performatives = frozenset( - { - LedgerApiMessage.Performative.BALANCE, - LedgerApiMessage.Performative.RAW_TRANSACTION, - LedgerApiMessage.Performative.TRANSACTION_DIGEST, - LedgerApiMessage.Performative.TRANSACTION_RECEIPT, - LedgerApiMessage.Performative.ERROR, - LedgerApiMessage.Performative.STATE, - } - ) - - -class ContractApiHandler(AbstractResponseHandler): - """Implement the contract api handler.""" - - SUPPORTED_PROTOCOL: Optional[PublicId] = ContractApiMessage.protocol_id - allowed_response_performatives = frozenset( - { - ContractApiMessage.Performative.RAW_TRANSACTION, - ContractApiMessage.Performative.RAW_MESSAGE, - ContractApiMessage.Performative.ERROR, - ContractApiMessage.Performative.STATE, - } - ) - - -class TendermintHandler(Handler): - """ - The Tendermint config-sharing request / response handler. - - This handler is used to share the information necessary - to set up the Tendermint network. The agents use it during - the RegistrationStartupBehaviour, and communicate with - each other over the Agent Communication Network using a - p2p_libp2p or p2p_libp2p_client connection. - - This handler does NOT use the ABCI connection. - """ - - SUPPORTED_PROTOCOL: Optional[PublicId] = TendermintMessage.protocol_id - - class LogMessages(Enum): - """Log messages used in the TendermintHandler""" - - unidentified_dialogue = "Unidentified Tendermint dialogue" - no_addresses_retrieved_yet = "No registered addresses retrieved yet" - not_in_registered_addresses = "Sender not registered for on-chain service" - sending_request_response = "Sending Tendermint request response" - failed_to_parse_address = "Failed to parse Tendermint network address" - failed_to_parse_params = ( - "Failed to parse Tendermint recovery parameters from message" - ) - collected_config_info = "Collected Tendermint config info" - collected_params = "Collected Tendermint recovery parameters" - received_error_without_target_message = ( - "Received error message but could not retrieve target message" - ) - received_error_response = "Received error response" - sending_error_response = "Sending error response" - performative_not_recognized = "Performative not recognized" - - def __str__(self) -> str: # pragma: no cover - """For ease of use in formatted string literals""" - return self.value - - def setup(self) -> None: - """Set up the handler.""" - - def teardown(self) -> None: - """Tear down the handler.""" - - @property - def initial_tm_configs(self) -> Dict[str, Dict[str, Any]]: - """A mapping of the other agents' addresses to their initial Tendermint configuration.""" - return self.context.state.initial_tm_configs - - @initial_tm_configs.setter - def initial_tm_configs(self, configs: Dict[str, Dict[str, Any]]) -> None: - """A mapping of the other agents' addresses to their initial Tendermint configuration.""" - self.context.state.initial_tm_configs = configs - - @property - def dialogues(self) -> Optional[TendermintDialogues]: - """Tendermint config-sharing request / response protocol dialogues""" - - attribute = cast(PublicId, self.SUPPORTED_PROTOCOL).name + "_dialogues" - return getattr(self.context, attribute, None) - - def handle(self, message: Message) -> None: - """Handle incoming Tendermint config-sharing messages""" - - dialogues = cast(TendermintDialogues, self.dialogues) - dialogue = cast(TendermintDialogue, dialogues.update(message)) - - if dialogue is None: - log_message = self.LogMessages.unidentified_dialogue.value - self.context.logger.error(f"{log_message}: {message}") - return - - message = cast(TendermintMessage, message) - handler_name = f"_{message.performative.value}" - handler = getattr(self, handler_name, None) - if handler is None: - log_message = self.LogMessages.performative_not_recognized.value - self.context.logger.error(f"{log_message}: {message}") - return - - handler(message, dialogue) - - def _reply_with_tendermint_error( - self, - message: TendermintMessage, - dialogue: TendermintDialogue, - error_message: str, - ) -> None: - """Reply with Tendermint config-sharing error""" - response = dialogue.reply( - performative=TendermintMessage.Performative.ERROR, - target_message=message, - error_code=TendermintMessage.ErrorCode.INVALID_REQUEST, - error_msg=error_message, - error_data={}, - ) - self.context.outbox.put_message(response) - log_message = self.LogMessages.sending_error_response.value - log_message += f". Received: {message}, replied: {response}" - self.context.logger.error(log_message) - - def _not_registered_error( - self, message: TendermintMessage, dialogue: TendermintDialogue - ) -> None: - """Check if sender is among on-chain registered addresses""" - # do not respond to errors to avoid loops - log_message = self.LogMessages.not_in_registered_addresses.value - self.context.logger.error(f"{log_message}: {message}") - self._reply_with_tendermint_error(message, dialogue, log_message) - - def _check_registered( - self, message: TendermintMessage, dialogue: TendermintDialogue - ) -> bool: - """Check if the sender is registered on-chain and if not, reply with an error""" - others_addresses = self.context.state.acn_container() - if message.sender in others_addresses: - return True - - self._not_registered_error(message, dialogue) - return False - - def _get_genesis_info( - self, message: TendermintMessage, dialogue: TendermintDialogue - ) -> None: - """Handler Tendermint config-sharing request message""" - - if not self._check_registered(message, dialogue): - return - info = self.initial_tm_configs.get(self.context.agent_address, None) - if info is None: - log_message = self.LogMessages.no_addresses_retrieved_yet.value - self.context.logger.info(f"{log_message}: {message}") - self._reply_with_tendermint_error(message, dialogue, log_message) - return - - response = dialogue.reply( - performative=TendermintMessage.Performative.GENESIS_INFO, - target_message=message, - info=json.dumps(info), - ) - self.context.outbox.put_message(message=response) - log_message = self.LogMessages.sending_request_response.value - self.context.logger.info(f"{log_message}: {response}") - - def _get_recovery_params( - self, message: TendermintMessage, dialogue: TendermintDialogue - ) -> None: - """Handle a request message for the recovery parameters.""" - if not self._check_registered(message, dialogue): - return - - shared_state = cast(SharedState, self.context.state) - recovery_params = shared_state.tm_recovery_params - response = dialogue.reply( - performative=TendermintMessage.Performative.RECOVERY_PARAMS, - target_message=message, - params=json.dumps(asdict(recovery_params)), - ) - self.context.outbox.put_message(message=response) - log_message = self.LogMessages.sending_request_response.value - self.context.logger.info(f"{log_message}: {response}") - - def _genesis_info( - self, message: TendermintMessage, dialogue: TendermintDialogue - ) -> None: - """Process Tendermint config-sharing response messages""" - - if not self._check_registered(message, dialogue): - return - - try: # validate message contains a valid address - validator_config = json.loads(message.info) - self.context.logger.info(f"Validator config received: {validator_config}") - hostname = cast(str, validator_config["hostname"]) - if hostname != "localhost" and not hostname.startswith("node"): - ipaddress.ip_network(hostname) - except (KeyError, ValueError) as e: - log_message = self.LogMessages.failed_to_parse_address.value - self.context.logger.error(f"{log_message}: {e} {message}") - self._reply_with_tendermint_error(message, dialogue, log_message) - return - - initial_tm_configs = self.initial_tm_configs - initial_tm_configs[message.sender] = validator_config - self.initial_tm_configs = initial_tm_configs - log_message = self.LogMessages.collected_config_info.value - self.context.logger.info(f"{log_message}: {message}") - dialogues = cast(TendermintDialogues, self.dialogues) - dialogues.dialogue_stats.add_dialogue_endstate( - TendermintDialogue.EndState.COMMUNICATED, dialogue.is_self_initiated - ) - - def _recovery_params( - self, message: TendermintMessage, dialogue: TendermintDialogue - ) -> None: - """Process params-sharing response messages.""" - - if not self._check_registered(message, dialogue): - return - - try: - recovery_params = json.loads(message.params) - shared_state = cast(SharedState, self.context.state) - shared_state.address_to_acn_deliverable[ - message.sender - ] = TendermintRecoveryParams(**recovery_params) - except (json.JSONDecodeError, TypeError) as exc: - log_message = self.LogMessages.failed_to_parse_params.value - self.context.logger.error(f"{log_message}: {exc} {message}") - self._reply_with_tendermint_error(message, dialogue, log_message) - return - - log_message = self.LogMessages.collected_params.value - self.context.logger.info(f"{log_message}: {message}") - dialogues = cast(TendermintDialogues, self.dialogues) - dialogues.dialogue_stats.add_dialogue_endstate( - TendermintDialogue.EndState.COMMUNICATED, dialogue.is_self_initiated - ) - - def _error(self, message: TendermintMessage, dialogue: TendermintDialogue) -> None: - """Handle error message as response""" - - target_message = dialogue.get_message_by_id(message.target) - if not target_message: - log_message = self.LogMessages.received_error_without_target_message.value - self.context.logger.error(log_message) - return - - log_message = self.LogMessages.received_error_response.value - log_message += f". Received: {message}, in reply to: {target_message}" - self.context.logger.error(log_message) - dialogues = cast(TendermintDialogues, self.dialogues) - dialogues.dialogue_stats.add_dialogue_endstate( - TendermintDialogue.EndState.NOT_COMMUNICATED, dialogue.is_self_initiated - ) - - -class IpfsHandler(AbstractResponseHandler): - """A class for handling IPFS messages.""" - - SUPPORTED_PROTOCOL: Optional[PublicId] = IpfsMessage.protocol_id - allowed_response_performatives = frozenset( - { - IpfsMessage.Performative.IPFS_HASH, - IpfsMessage.Performative.FILES, - IpfsMessage.Performative.ERROR, - } - ) diff --git a/packages/valory/skills/abstract_round_abci/io_/__init__.py b/packages/valory/skills/abstract_round_abci/io_/__init__.py deleted file mode 100644 index 346ca66..0000000 --- a/packages/valory/skills/abstract_round_abci/io_/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains all the input-output operations logic of the behaviours.""" # pragma: nocover diff --git a/packages/valory/skills/abstract_round_abci/io_/ipfs.py b/packages/valory/skills/abstract_round_abci/io_/ipfs.py deleted file mode 100644 index cfd914c..0000000 --- a/packages/valory/skills/abstract_round_abci/io_/ipfs.py +++ /dev/null @@ -1,85 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains all the interaction operations of the behaviours with IPFS.""" - - -import os -from typing import Any, Dict, Optional, Type - -from packages.valory.skills.abstract_round_abci.io_.load import ( - CustomLoaderType, - Loader, - SupportedFiletype, - SupportedObjectType, -) -from packages.valory.skills.abstract_round_abci.io_.store import ( - CustomStorerType, - Storer, -) - - -class IPFSInteractionError(Exception): - """A custom exception for IPFS interaction errors.""" - - -class IPFSInteract: - """Class for interacting with IPFS.""" - - def __init__(self, loader_cls: Type = Loader, storer_cls: Type = Storer): - """Initialize an `IPFSInteract` object.""" - # Set loader/storer class. - self._loader_cls = loader_cls - self._storer_cls = storer_cls - - def store( - self, - filepath: str, - obj: SupportedObjectType, - multiple: bool, - filetype: Optional[SupportedFiletype] = None, - custom_storer: Optional[CustomStorerType] = None, - **kwargs: Any, - ) -> Dict[str, str]: - """Temporarily store a file locally, in order to send it to IPFS and retrieve a hash, and then delete it.""" - filepath = os.path.normpath(filepath) - if multiple: - # Add trailing slash in order to treat path as a folder. - filepath = os.path.join(filepath, "") - storer = self._storer_cls(filetype, custom_storer, filepath) - - try: - name_to_obj = storer.store(obj, multiple, **kwargs) - return name_to_obj - except Exception as e: # pylint: disable=broad-except - raise IPFSInteractionError(str(e)) from e - - def load( # pylint: disable=too-many-arguments - self, - serialized_objects: Dict[str, str], - filetype: Optional[SupportedFiletype] = None, - custom_loader: CustomLoaderType = None, - ) -> SupportedObjectType: - """Deserialize objects received via IPFS.""" - loader = self._loader_cls(filetype, custom_loader) - try: - deserialized_objects = loader.load(serialized_objects) - return deserialized_objects - except Exception as e: # pylint: disable=broad-except - raise IPFSInteractionError(str(e)) from e diff --git a/packages/valory/skills/abstract_round_abci/io_/load.py b/packages/valory/skills/abstract_round_abci/io_/load.py deleted file mode 100644 index c9ceafb..0000000 --- a/packages/valory/skills/abstract_round_abci/io_/load.py +++ /dev/null @@ -1,124 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains all the loading operations of the behaviours.""" - -import json -from abc import ABC, abstractmethod -from typing import Any, Callable, Dict, Optional - -from packages.valory.skills.abstract_round_abci.io_.store import ( - CustomObjectType, - NativelySupportedSingleObjectType, - SupportedFiletype, - SupportedObjectType, - SupportedSingleObjectType, -) - - -CustomLoaderType = Optional[Callable[[str], CustomObjectType]] -SupportedLoaderType = Callable[[str], SupportedSingleObjectType] - - -class AbstractLoader(ABC): - """An abstract `Loader` class.""" - - @abstractmethod - def load_single_object( - self, serialized_object: str - ) -> NativelySupportedSingleObjectType: - """Load a single object.""" - - def load(self, serialized_objects: Dict[str, str]) -> SupportedObjectType: - """ - Load one or more serialized objects. - - :param serialized_objects: A mapping of filenames to serialized object they contained. - :return: the loaded file(s). - """ - if len(serialized_objects) == 0: - # no objects are present, raise an error - raise ValueError('"serialized_objects" does not contain any objects') - - objects = {} - for filename, body in serialized_objects.items(): - objects[filename] = self.load_single_object(body) - - if len(objects) > 1: - # multiple object are present - # we return them as mapping of - # names and their value - return objects - - # one object is present, we simply return it as an object, i.e. without its name - _name, deserialized_body = objects.popitem() - return deserialized_body - - -class JSONLoader(AbstractLoader): - """A JSON file loader.""" - - def load_single_object( - self, serialized_object: str - ) -> NativelySupportedSingleObjectType: - """Read a json file. - - :param serialized_object: the file serialized into a JSON string. - :return: the deserialized json file's content. - """ - try: - deserialized_file = json.loads(serialized_object) - return deserialized_file - except json.JSONDecodeError as e: # pragma: no cover - raise IOError( - f"File '{serialized_object}' has an invalid JSON encoding!" - ) from e - except ValueError as e: # pragma: no cover - raise IOError( - f"There is an encoding error in the '{serialized_object}' file!" - ) from e - - -class Loader(AbstractLoader): - """Class which loads objects.""" - - def __init__(self, filetype: Optional[Any], custom_loader: CustomLoaderType): - """Initialize a `Loader`.""" - self._filetype = filetype - self._custom_loader = custom_loader - self.__filetype_to_loader: Dict[SupportedFiletype, SupportedLoaderType] = { - SupportedFiletype.JSON: JSONLoader().load_single_object, - } - - def load_single_object(self, serialized_object: str) -> SupportedSingleObjectType: - """Load a single file.""" - loader = self._get_single_loader_from_filetype() - return loader(serialized_object) - - def _get_single_loader_from_filetype(self) -> SupportedLoaderType: - """Get an object loader from a given filetype or keep a custom loader.""" - if self._filetype is not None: - return self.__filetype_to_loader[self._filetype] - - if self._custom_loader is not None: # pragma: no cover - return self._custom_loader - - raise ValueError( # pragma: no cover - "Please provide either a supported filetype or a custom loader function." - ) diff --git a/packages/valory/skills/abstract_round_abci/io_/paths.py b/packages/valory/skills/abstract_round_abci/io_/paths.py deleted file mode 100644 index 40b8923..0000000 --- a/packages/valory/skills/abstract_round_abci/io_/paths.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains all the path related operations of the behaviours.""" - - -import os - - -def create_pathdirs(path: str) -> None: - """Create the non-existing directories of a given path. - - :param path: the given path. - """ - dirname = os.path.dirname(path) - - if dirname: - os.makedirs(dirname, exist_ok=True) diff --git a/packages/valory/skills/abstract_round_abci/io_/store.py b/packages/valory/skills/abstract_round_abci/io_/store.py deleted file mode 100644 index 588dc36..0000000 --- a/packages/valory/skills/abstract_round_abci/io_/store.py +++ /dev/null @@ -1,153 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains all the storing operations of the behaviours.""" - - -import json -import os.path -from abc import ABC, abstractmethod -from enum import Enum, auto -from typing import Any, Callable, Dict, Optional, TypeVar, Union, cast - -from packages.valory.skills.abstract_round_abci.io_.paths import create_pathdirs - - -StoredJSONType = Union[dict, list] -NativelySupportedSingleObjectType = StoredJSONType -NativelySupportedMultipleObjectsType = Dict[str, NativelySupportedSingleObjectType] -NativelySupportedObjectType = Union[ - NativelySupportedSingleObjectType, NativelySupportedMultipleObjectsType -] -NativelySupportedStorerType = Callable[[str, NativelySupportedObjectType, Any], None] -CustomObjectType = TypeVar("CustomObjectType") -CustomStorerType = Callable[[str, CustomObjectType, Any], None] -SupportedSingleObjectType = Union[NativelySupportedObjectType, CustomObjectType] -SupportedMultipleObjectsType = Dict[str, SupportedSingleObjectType] -SupportedObjectType = Union[SupportedSingleObjectType, SupportedMultipleObjectsType] -SupportedStorerType = Union[NativelySupportedStorerType, CustomStorerType] -NativelySupportedJSONStorerType = Callable[ - [str, Union[StoredJSONType, Dict[str, StoredJSONType]], Any], None -] - - -class SupportedFiletype(Enum): - """Enum for the supported filetypes of the IPFS interacting methods.""" - - JSON = auto() - - -class AbstractStorer(ABC): - """An abstract `Storer` class.""" - - def __init__(self, path: str): - """Initialize an abstract storer.""" - self._path = path - # Create the dirs of the path if it does not exist. - create_pathdirs(path) - - @abstractmethod - def serialize_object( - self, filename: str, obj: SupportedSingleObjectType, **kwargs: Any - ) -> Dict[str, str]: - """Store a single file.""" - - def store( - self, obj: SupportedObjectType, multiple: bool, **kwargs: Any - ) -> Dict[str, str]: - """Serialize one or multiple objects.""" - serialized_files: Dict[str, str] = {} - if multiple: - if not isinstance(obj, dict): # pragma: no cover - raise ValueError( - f"Cannot store multiple files of type {type(obj)}!" - f"Should be a dictionary of filenames mapped to their objects." - ) - for filename, single_obj in obj.items(): - filename = os.path.join(self._path, filename) - serialized_file = self.serialize_object(filename, single_obj, **kwargs) - serialized_files.update(**serialized_file) - else: - serialized_file = self.serialize_object(self._path, obj, **kwargs) - serialized_files.update(**serialized_file) - return serialized_files - - -class JSONStorer(AbstractStorer): - """A JSON file storer.""" - - def serialize_object( - self, filename: str, obj: NativelySupportedSingleObjectType, **kwargs: Any - ) -> Dict[str, str]: - """ - Serialize an object to JSON. - - :param filename: under which name the provided object should be serialized. Note that it will appear in IPFS with this name. - :param obj: the object to store. - :returns: a dict mapping the name to the serialized object. - """ - if not any(isinstance(obj, type_) for type_ in (dict, list)): - raise ValueError( # pragma: no cover - f"`JSONStorer` cannot be used with a {type(obj)}! Only with a {StoredJSONType}" - ) - try: - serialized_object = json.dumps(obj, ensure_ascii=False, indent=4) - name_to_obj = {filename: serialized_object} - return name_to_obj - except (TypeError, OSError) as e: # pragma: no cover - raise IOError(str(e)) from e - - -class Storer(AbstractStorer): - """Class which serializes objects.""" - - def __init__( - self, - filetype: Optional[Any], - custom_storer: Optional[CustomStorerType], - path: str, - ): - """Initialize a `Storer`.""" - super().__init__(path) - self._filetype = filetype - self._custom_storer = custom_storer - self._filetype_to_storer: Dict[Enum, SupportedStorerType] = { - SupportedFiletype.JSON: cast( - NativelySupportedJSONStorerType, JSONStorer(path).serialize_object - ), - } - - def serialize_object( - self, filename: str, obj: NativelySupportedObjectType, **kwargs: Any - ) -> Dict[str, str]: - """Store a single object.""" - storer = self._get_single_storer_from_filetype() - return storer(filename, obj, **kwargs) # type: ignore - - def _get_single_storer_from_filetype(self) -> SupportedStorerType: - """Get an object storer from a given filetype or keep a custom storer.""" - if self._filetype is not None: - return self._filetype_to_storer[self._filetype] - - if self._custom_storer is not None: # pragma: no cover - return self._custom_storer - - raise ValueError( # pragma: no cover - "Please provide either a supported filetype or a custom storing function." - ) diff --git a/packages/valory/skills/abstract_round_abci/models.py b/packages/valory/skills/abstract_round_abci/models.py deleted file mode 100644 index 27304eb..0000000 --- a/packages/valory/skills/abstract_round_abci/models.py +++ /dev/null @@ -1,893 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the core models for all the ABCI apps.""" - -import inspect -import json -from abc import ABC, ABCMeta -from collections import Counter -from dataclasses import dataclass -from enum import Enum -from pathlib import Path -from time import time -from typing import ( - Any, - Callable, - Dict, - List, - Optional, - OrderedDict, - Tuple, - Type, - cast, - get_type_hints, -) - -from aea.configurations.data_types import PublicId -from aea.exceptions import enforce -from aea.skills.base import Model, SkillContext - -from packages.valory.protocols.http.message import HttpMessage -from packages.valory.skills.abstract_round_abci.base import ( - AbciApp, - AbciAppDB, - BaseSynchronizedData, - OffenceStatus, - ROUND_COUNT_DEFAULT, - RoundSequence, - VALUE_NOT_PROVIDED, - get_name, -) -from packages.valory.skills.abstract_round_abci.utils import ( - check, - check_type, - consensus_threshold, - get_data_from_nested_dict, - get_value_with_type, -) - - -MIN_RESET_PAUSE_DURATION = 10 -NUMBER_OF_RETRIES: int = 5 -DEFAULT_BACKOFF_FACTOR: float = 2.0 -DEFAULT_TYPE_NAME: str = "str" -DEFAULT_CHAIN = "ethereum" - - -class FrozenMixin: # pylint: disable=too-few-public-methods - """Mixin for classes to enforce read-only attributes.""" - - _frozen: bool = False - - def __delattr__(self, *args: Any) -> None: - """Override __delattr__ to make object immutable.""" - if self._frozen: - raise AttributeError( - "This object is frozen! To unfreeze switch `self._frozen` via `__dict__`." - ) - super().__delattr__(*args) - - def __setattr__(self, *args: Any) -> None: - """Override __setattr__ to make object immutable.""" - if self._frozen: - raise AttributeError( - "This object is frozen! To unfreeze switch `self._frozen` via `__dict__`." - ) - super().__setattr__(*args) - - -class TypeCheckMixin: # pylint: disable=too-few-public-methods - """Mixin for data classes & models to enforce attribute types on construction.""" - - def __post_init__(self) -> None: - """Check that the type of the provided attributes is correct.""" - for attr, type_ in get_type_hints(self).items(): - value = getattr(self, attr) - check_type(attr, value, type_) - - @classmethod - def _ensure(cls, key: str, kwargs: Dict, type_: Any) -> Any: - """Get and ensure the configuration field is not None (if no default is provided) and of correct type.""" - enforce("skill_context" in kwargs, "Only use on models!") - skill_id = kwargs["skill_context"].skill_id - enforce( - key in kwargs, - f"'{key}' of type '{type_}' required, but it is not set in `models.params.args` of `skill.yaml` of `{skill_id}`", - ) - value = kwargs.pop(key) - try: - check_type(key, value, type_) - except TypeError: # pragma: nocover - enforce( - False, - f"'{key}' must be a {type_}, but type {type(value)} was found in `models.params.args` of `skill.yaml` of `{skill_id}`", - ) - return value - - -@dataclass(frozen=True) -class GenesisBlock(TypeCheckMixin): - """A dataclass to store the genesis block.""" - - max_bytes: str - max_gas: str - time_iota_ms: str - - def to_json(self) -> Dict[str, str]: - """Get a GenesisBlock instance as a json dictionary.""" - return { - "max_bytes": self.max_bytes, - "max_gas": self.max_gas, - "time_iota_ms": self.time_iota_ms, - } - - -@dataclass(frozen=True) -class GenesisEvidence(TypeCheckMixin): - """A dataclass to store the genesis evidence.""" - - max_age_num_blocks: str - max_age_duration: str - max_bytes: str - - def to_json(self) -> Dict[str, str]: - """Get a GenesisEvidence instance as a json dictionary.""" - return { - "max_age_num_blocks": self.max_age_num_blocks, - "max_age_duration": self.max_age_duration, - "max_bytes": self.max_bytes, - } - - -@dataclass(frozen=True) -class GenesisValidator(TypeCheckMixin): - """A dataclass to store the genesis validator.""" - - pub_key_types: Tuple[str, ...] - - def to_json(self) -> Dict[str, List[str]]: - """Get a GenesisValidator instance as a json dictionary.""" - return {"pub_key_types": list(self.pub_key_types)} - - -@dataclass(frozen=True) -class GenesisConsensusParams(TypeCheckMixin): - """A dataclass to store the genesis consensus parameters.""" - - block: GenesisBlock - evidence: GenesisEvidence - validator: GenesisValidator - version: dict - - @classmethod - def from_json_dict(cls, json_dict: dict) -> "GenesisConsensusParams": - """Get a GenesisConsensusParams instance from a json dictionary.""" - block = GenesisBlock(**json_dict["block"]) - evidence = GenesisEvidence(**json_dict["evidence"]) - validator = GenesisValidator(tuple(json_dict["validator"]["pub_key_types"])) - return cls(block, evidence, validator, json_dict["version"]) - - def to_json(self) -> Dict[str, Any]: - """Get a GenesisConsensusParams instance as a json dictionary.""" - return { - "block": self.block.to_json(), - "evidence": self.evidence.to_json(), - "validator": self.validator.to_json(), - "version": self.version, - } - - -@dataclass(frozen=True) -class GenesisConfig(TypeCheckMixin): - """A dataclass to store the genesis configuration.""" - - genesis_time: str - chain_id: str - consensus_params: GenesisConsensusParams - voting_power: str - - @classmethod - def from_json_dict(cls, json_dict: dict) -> "GenesisConfig": - """Get a GenesisConfig instance from a json dictionary.""" - consensus_params = GenesisConsensusParams.from_json_dict( - json_dict["consensus_params"] - ) - return cls( - json_dict["genesis_time"], - json_dict["chain_id"], - consensus_params, - json_dict["voting_power"], - ) - - def to_json(self) -> Dict[str, Any]: - """Get a GenesisConfig instance as a json dictionary.""" - return { - "genesis_time": self.genesis_time, - "chain_id": self.chain_id, - "consensus_params": self.consensus_params.to_json(), - "voting_power": self.voting_power, - } - - -class BaseParams( - Model, FrozenMixin, TypeCheckMixin -): # pylint: disable=too-many-instance-attributes - """Parameters.""" - - def __init__(self, *args: Any, **kwargs: Any) -> None: - """ - Initialize the parameters object. - - The genesis configuration should be a dictionary with the following format: - genesis_time: str - chain_id: str - consensus_params: - block: - max_bytes: str - max_gas: str - time_iota_ms: str - evidence: - max_age_num_blocks: str - max_age_duration: str - max_bytes: str - validator: - pub_key_types: List[str] - version: dict - voting_power: str - - :param args: positional arguments - :param kwargs: keyword arguments - """ - self.genesis_config: GenesisConfig = GenesisConfig.from_json_dict( - self._ensure("genesis_config", kwargs, dict) - ) - self.service_id: str = self._ensure("service_id", kwargs, str) - self.tendermint_url: str = self._ensure("tendermint_url", kwargs, str) - self.max_healthcheck: int = self._ensure("max_healthcheck", kwargs, int) - self.round_timeout_seconds: float = self._ensure( - "round_timeout_seconds", kwargs, float - ) - self.sleep_time: int = self._ensure("sleep_time", kwargs, int) - self.retry_timeout: int = self._ensure("retry_timeout", kwargs, int) - self.retry_attempts: int = self._ensure("retry_attempts", kwargs, int) - self.keeper_timeout: float = self._ensure("keeper_timeout", kwargs, float) - self.reset_pause_duration: int = self._ensure_gte( - "reset_pause_duration", kwargs, int, min_value=MIN_RESET_PAUSE_DURATION - ) - self.drand_public_key: str = self._ensure("drand_public_key", kwargs, str) - self.tendermint_com_url: str = self._ensure("tendermint_com_url", kwargs, str) - self.tendermint_max_retries: int = self._ensure( - "tendermint_max_retries", kwargs, int - ) - self.tendermint_check_sleep_delay: int = self._ensure( - "tendermint_check_sleep_delay", kwargs, int - ) - self.reset_tendermint_after: int = self._ensure( - "reset_tendermint_after", kwargs, int - ) - self.cleanup_history_depth: int = self._ensure( - "cleanup_history_depth", kwargs, int - ) - self.cleanup_history_depth_current: Optional[int] = self._ensure( - "cleanup_history_depth_current", kwargs, Optional[int] - ) - self.request_timeout: float = self._ensure("request_timeout", kwargs, float) - self.request_retry_delay: float = self._ensure( - "request_retry_delay", kwargs, float - ) - self.tx_timeout: float = self._ensure("tx_timeout", kwargs, float) - self.max_attempts: int = self._ensure("max_attempts", kwargs, int) - self.service_registry_address: Optional[str] = self._ensure( - "service_registry_address", kwargs, Optional[str] - ) - self.on_chain_service_id: Optional[int] = self._ensure( - "on_chain_service_id", kwargs, Optional[int] - ) - self.share_tm_config_on_startup: bool = self._ensure( - "share_tm_config_on_startup", kwargs, bool - ) - self.tendermint_p2p_url: str = self._ensure("tendermint_p2p_url", kwargs, str) - self.use_termination: bool = self._ensure("use_termination", kwargs, bool) - self.use_slashing: bool = self._ensure("use_slashing", kwargs, bool) - self.slash_cooldown_hours: int = self._ensure( - "slash_cooldown_hours", kwargs, int - ) - self.slash_threshold_amount: int = self._ensure( - "slash_threshold_amount", kwargs, int - ) - self.light_slash_unit_amount: int = self._ensure( - "light_slash_unit_amount", kwargs, int - ) - self.serious_slash_unit_amount: int = self._ensure( - "serious_slash_unit_amount", kwargs, int - ) - self.setup_params: Dict[str, Any] = self._ensure("setup", kwargs, dict) - # TODO add to all configs - self.default_chain_id: str = kwargs.get("default_chain_id", DEFAULT_CHAIN) - - # we sanitize for null values as these are just kept for schema definitions - skill_id = kwargs["skill_context"].skill_id - super().__init__(*args, **kwargs) - - if not self.context.is_abstract_component: - # setup data are mandatory for non-abstract skills, - # and they should always contain at least `all_participants` and `safe_contract_address` - self._ensure_setup( - { - get_name(BaseSynchronizedData.safe_contract_address): str, - get_name(BaseSynchronizedData.all_participants): List[str], - get_name(BaseSynchronizedData.consensus_threshold): cast( - Type, Optional[int] - ), - }, - skill_id, - ) - self._frozen = True - - def _ensure_setup( - self, necessary_params: Dict[str, Type], skill_id: PublicId - ) -> Any: - """Ensure that the `setup` params contain all the `necessary_keys` and have the correct types.""" - enforce(bool(self.setup_params), "`setup` params contain no values!") - - for key, type_ in necessary_params.items(): - # check that the key is present, note that None is acceptable for optional keys - value = self.setup_params.get(key, VALUE_NOT_PROVIDED) - if value is VALUE_NOT_PROVIDED: - fail_msg = f"Value for `{key}` missing from the `setup` params." - enforce(False, fail_msg) - - # check that the value is of the correct type - try: - check_type(key, value, type_) - except TypeError: # pragma: nocover - enforce( - False, - f"'{key}' must be a {type_}, but type {type(value)} was found in `models.params.args.setup` " - f"of `skill.yaml` of `{skill_id}`", - ) - - def _ensure_gte( - self, key: str, kwargs: Dict[str, Any], type_: Type, min_value: Any - ) -> Any: - """Ensure that the value for the key is greater than or equal to the provided min_value.""" - err = check(min_value, type_) - enforce( - err is None, - f"min_value must be of type {type_.__name__}, but got {type(min_value).__name__}.", - ) - value = self._ensure(key, kwargs, type_) - enforce( - value >= min_value, f"`{key}` must be greater than or equal to {min_value}." - ) - return value - - -class _MetaSharedState(ABCMeta): - """A metaclass that validates SharedState's attributes.""" - - def __new__(mcs, name: str, bases: Tuple, namespace: Dict, **kwargs: Any) -> Type: # type: ignore - """Initialize the class.""" - new_cls = super().__new__(mcs, name, bases, namespace, **kwargs) - - if ABC in bases: - # abstract class, return - return new_cls - if not issubclass(new_cls, SharedState): - # the check only applies to SharedState subclasses - return new_cls - - mcs._check_consistency(cast(Type[SharedState], new_cls)) - return new_cls - - @classmethod - def _check_consistency(mcs, shared_state_cls: Type["SharedState"]) -> None: - """Check consistency of class attributes.""" - mcs._check_required_class_attributes(shared_state_cls) - - @classmethod - def _check_required_class_attributes( - mcs, shared_state_cls: Type["SharedState"] - ) -> None: - """Check that required class attributes are set.""" - if not hasattr(shared_state_cls, "abci_app_cls"): - raise AttributeError(f"'abci_app_cls' not set on {shared_state_cls}") - abci_app_cls = shared_state_cls.abci_app_cls - if not inspect.isclass(abci_app_cls): - raise AttributeError(f"The object `{abci_app_cls}` is not a class") - if not issubclass(abci_app_cls, AbciApp): - cls_name = AbciApp.__name__ - cls_module = AbciApp.__module__ - raise AttributeError( - f"The class {abci_app_cls} is not an instance of {cls_module}.{cls_name}" - ) - - -class SharedState(Model, ABC, metaclass=_MetaSharedState): # type: ignore - """Keep the current shared state of the skill.""" - - abci_app_cls: Type[AbciApp] - - def __init__( - self, - *args: Any, - skill_context: SkillContext, - **kwargs: Any, - ) -> None: - """Initialize the state.""" - self.abci_app_cls._is_abstract = skill_context.is_abstract_component - self._round_sequence: Optional[RoundSequence] = None - # a mapping of the agents' addresses to their initial Tendermint configuration, to be retrieved via ACN - self.initial_tm_configs: Dict[str, Optional[Dict[str, Any]]] = {} - # a mapping of the other agents' addresses to ACN deliverables - self.address_to_acn_deliverable: Dict[str, Any] = {} - self.tm_recovery_params: TendermintRecoveryParams = TendermintRecoveryParams( - self.abci_app_cls.initial_round_cls.auto_round_id() - ) - kwargs["skill_context"] = skill_context - super().__init__(*args, **kwargs) - - def setup_slashing(self, validator_to_agent: Dict[str, str]) -> None: - """Initialize the structures required for slashing.""" - configured_agents = set(self.initial_tm_configs.keys()) - agents_mapped = set(validator_to_agent.values()) - diff = agents_mapped.symmetric_difference(configured_agents) - if diff: - raise ValueError( - f"Trying to use the mapping `{validator_to_agent}`, which contains validators for non-configured " - "agents and/or does not contain validators for some configured agents. " - f"The agents which have been configured via ACN are `{configured_agents}` and the diff was for {diff}." - ) - self.round_sequence.validator_to_agent = validator_to_agent - self.round_sequence.offence_status = { - agent: OffenceStatus() for agent in agents_mapped - } - - def get_validator_address(self, agent_address: str) -> str: - """Get the validator address of an agent.""" - if agent_address not in self.synchronized_data.all_participants: - raise ValueError( - f"The validator address of non-participating agent `{agent_address}` was requested." - ) - - try: - agent_config = self.initial_tm_configs[agent_address] - except KeyError as e: - raise ValueError( - "SharedState's setup was not performed successfully." - ) from e - - if agent_config is None: - raise ValueError( - f"ACN registration has not been successfully performed for agent `{agent_address}`. " - "Have you set the `share_tm_config_on_startup` flag to `true` in the configuration?" - ) - - validator_address = agent_config.get("address", None) - if validator_address is None: - raise ValueError( - f"The tendermint configuration for agent `{agent_address}` is invalid: `{agent_config}`." - ) - - return validator_address - - def acn_container(self) -> Dict[str, Any]: - """Create a container for ACN results, i.e., a mapping from others' addresses to `None`.""" - ourself = {self.context.agent_address} - others_addresses = self.synchronized_data.all_participants - ourself - - return dict.fromkeys(others_addresses) - - def setup(self) -> None: - """Set up the model.""" - self._round_sequence = RoundSequence(self.context, self.abci_app_cls) - setup_params = cast(BaseParams, self.context.params).setup_params - self.round_sequence.setup( - BaseSynchronizedData( - AbciAppDB( - setup_data=AbciAppDB.data_to_lists(setup_params), - cross_period_persisted_keys=self.abci_app_cls.cross_period_persisted_keys, - logger=self.context.logger, - ) - ), - self.context.logger, - ) - if not self.context.is_abstract_component: - self.initial_tm_configs = dict.fromkeys( - self.synchronized_data.all_participants - ) - - @property - def round_sequence(self) -> RoundSequence: - """Get the round_sequence.""" - if self._round_sequence is None: - raise ValueError("round sequence not available") - return self._round_sequence - - @property - def synchronized_data(self) -> BaseSynchronizedData: - """Get the latest synchronized_data if available.""" - return self.round_sequence.latest_synchronized_data - - def get_acn_result(self) -> Any: - """Get the majority of the ACN deliverables.""" - if len(self.address_to_acn_deliverable) == 0: - return None - - # the current agent does not participate, so we need `nb_participants - 1` - threshold = consensus_threshold(self.synchronized_data.nb_participants - 1) - counter = Counter(self.address_to_acn_deliverable.values()) - most_common_value, n_appearances = counter.most_common(1)[0] - - if n_appearances < threshold: - return None - - self.context.logger.debug( - f"ACN result is '{most_common_value}' from '{self.address_to_acn_deliverable}'." - ) - return most_common_value - - -class Requests(Model, FrozenMixin): - """Keep the current pending requests.""" - - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Initialize the state.""" - # mapping from dialogue reference nonce to a callback - self.request_id_to_callback: Dict[str, Callable] = {} - super().__init__(*args, **kwargs) - self._frozen = True - - -class UnexpectedResponseError(Exception): - """Exception class for unexpected responses from Apis.""" - - -@dataclass -class ResponseInfo(TypeCheckMixin): - """A dataclass to hold all the information related to the response.""" - - response_key: Optional[str] - response_index: Optional[int] - response_type: str - error_key: Optional[str] - error_index: Optional[int] - error_type: str - error_data: Any = None - - @classmethod - def from_json_dict(cls, kwargs: Dict) -> "ResponseInfo": - """Initialize a response info object from kwargs.""" - response_key: Optional[str] = kwargs.pop("response_key", None) - response_index: Optional[int] = kwargs.pop("response_index", None) - response_type: str = kwargs.pop("response_type", DEFAULT_TYPE_NAME) - error_key: Optional[str] = kwargs.pop("error_key", None) - error_index: Optional[int] = kwargs.pop("error_index", None) - error_type: str = kwargs.pop("error_type", DEFAULT_TYPE_NAME) - return cls( - response_key, - response_index, - response_type, - error_key, - error_index, - error_type, - ) - - -@dataclass -class RetriesInfo(TypeCheckMixin): - """A dataclass to hold all the information related to the retries.""" - - retries: int - backoff_factor: float - retries_attempted: int = 0 - - @classmethod - def from_json_dict(cls, kwargs: Dict) -> "RetriesInfo": - """Initialize a retries info object from kwargs.""" - retries: int = kwargs.pop("retries", NUMBER_OF_RETRIES) - backoff_factor: float = kwargs.pop("backoff_factor", DEFAULT_BACKOFF_FACTOR) - return cls(retries, backoff_factor) - - @property - def suggested_sleep_time(self) -> float: - """The suggested amount of time to sleep.""" - return self.backoff_factor**self.retries_attempted - - -@dataclass(frozen=True) -class TendermintRecoveryParams(TypeCheckMixin): - """ - A dataclass to hold all parameters related to agent <-> tendermint recovery procedures. - - This must be frozen so that we make sure it does not get edited. - """ - - reset_from_round: str - round_count: int = ROUND_COUNT_DEFAULT - reset_params: Optional[Dict[str, str]] = None - serialized_db_state: Optional[str] = None - - def __hash__(self) -> int: - """Hash the object.""" - return hash( - self.reset_from_round - + str(self.round_count) - + str(self.serialized_db_state) - + json.dumps(self.reset_params, sort_keys=True) - ) - - -class ApiSpecs(Model, FrozenMixin, TypeCheckMixin): - """A model that wraps APIs to get cryptocurrency prices.""" - - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Initialize ApiSpecsModel.""" - self.url: str = self._ensure("url", kwargs, str) - self.api_id: str = self._ensure("api_id", kwargs, str) - self.method: str = self._ensure("method", kwargs, str) - self.headers: Dict[str, str] = dict( - self._ensure("headers", kwargs, OrderedDict[str, str]) - ) - self.parameters: Dict[str, str] = dict( - self._ensure("parameters", kwargs, OrderedDict[str, str]) - ) - self.response_info = ResponseInfo.from_json_dict(kwargs) - self.retries_info = RetriesInfo.from_json_dict(kwargs) - super().__init__(*args, **kwargs) - self._frozen = True - - def get_spec( - self, - ) -> Dict: - """Returns dictionary containing api specifications.""" - - return { - "url": self.url, - "method": self.method, - "headers": self.headers, - "parameters": self.parameters, - } - - def _log_response(self, decoded_response: str) -> None: - """Log the decoded response message using error level.""" - pretty_json_str = json.dumps(decoded_response, indent=4) - self.context.logger.error(f"Response: {pretty_json_str}") - - @staticmethod - def _parse_response( - response_data: Any, - response_keys: Optional[str], - response_index: Optional[int], - response_type: str, - ) -> Any: - """Parse a response from an API.""" - if response_keys is not None: - response_data = get_data_from_nested_dict(response_data, response_keys) - - if response_index is not None: - response_data = response_data[response_index] - - return get_value_with_type(response_data, response_type) - - def _get_error_from_response(self, response_data: Any) -> Any: - """Try to get an error from the response.""" - try: - return self._parse_response( - response_data, - self.response_info.error_key, - self.response_info.error_index, - self.response_info.error_type, - ) - except (KeyError, IndexError, TypeError): - self.context.logger.error( - f"Could not parse error using the given key(s) ({self.response_info.error_key}) " - f"and index ({self.response_info.error_index})!" - ) - return None - - def _parse_response_data(self, response_data: Any) -> Any: - """Get the response data.""" - try: - return self._parse_response( - response_data, - self.response_info.response_key, - self.response_info.response_index, - self.response_info.response_type, - ) - except (KeyError, IndexError, TypeError) as e: - raise UnexpectedResponseError from e - - def process_response(self, response: HttpMessage) -> Any: - """Process response from api.""" - decoded_response = response.body.decode() - self.response_info.error_data = None - - try: - response_data = json.loads(decoded_response) - except json.JSONDecodeError: - self.context.logger.error("Could not parse the response body!") - self._log_response(decoded_response) - return None - - try: - return self._parse_response_data(response_data) - except UnexpectedResponseError: - self.context.logger.error( - f"Could not access response using the given key(s) ({self.response_info.response_key}) " - f"and index ({self.response_info.response_index})!" - ) - self._log_response(decoded_response) - self.response_info.error_data = self._get_error_from_response(response_data) - return None - - def increment_retries(self) -> None: - """Increment the retries counter.""" - self.retries_info.retries_attempted += 1 - - def reset_retries(self) -> None: - """Reset the retries counter.""" - self.retries_info.retries_attempted = 0 - - def is_retries_exceeded(self) -> bool: - """Check if the retries amount has been exceeded.""" - return self.retries_info.retries_attempted > self.retries_info.retries - - -class BenchmarkBlockTypes(Enum): - """Benchmark block types.""" - - LOCAL = "local" - CONSENSUS = "consensus" - TOTAL = "total" - - -class BenchmarkBlock: - """ - Benchmark - - This class represents logic to measure the code block using a - context manager. - """ - - start: float - total_time: float - block_type: str - - def __init__(self, block_type: str) -> None: - """Benchmark for single round.""" - self.block_type = block_type - self.start = 0 - self.total_time = 0 - - def __enter__( - self, - ) -> None: - """Enter context.""" - self.start = time() - - def __exit__(self, *args: List, **kwargs: Dict) -> None: - """Exit context""" - self.total_time = time() - self.start - - -class BenchmarkBehaviour: - """ - BenchmarkBehaviour - - This class represents logic to benchmark a single behaviour. - """ - - local_data: Dict[str, BenchmarkBlock] - - def __init__( - self, - ) -> None: - """Initialize Benchmark behaviour object.""" - self.local_data = {} - - def _measure(self, block_type: str) -> BenchmarkBlock: - """ - Returns a BenchmarkBlock object. - - :param block_type: type of block (e.g. local, consensus, request) - :return: BenchmarkBlock - """ - - if block_type not in self.local_data: - self.local_data[block_type] = BenchmarkBlock(block_type) - - return self.local_data[block_type] - - def local( - self, - ) -> BenchmarkBlock: - """Measure local block.""" - return self._measure(BenchmarkBlockTypes.LOCAL.value) - - def consensus( - self, - ) -> BenchmarkBlock: - """Measure consensus block.""" - return self._measure(BenchmarkBlockTypes.CONSENSUS.value) - - -class BenchmarkTool(Model, TypeCheckMixin, FrozenMixin): - """ - BenchmarkTool - - Tool to benchmark ABCI apps. - """ - - benchmark_data: Dict[str, BenchmarkBehaviour] - log_dir: Path - - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Benchmark tool for rounds behaviours.""" - self.benchmark_data = {} - log_dir_ = self._ensure("log_dir", kwargs, str) - self.log_dir = Path(log_dir_) - super().__init__(*args, **kwargs) - self._frozen = True - - def measure(self, behaviour: str) -> BenchmarkBehaviour: - """Measure time to complete round.""" - if behaviour not in self.benchmark_data: - self.benchmark_data[behaviour] = BenchmarkBehaviour() - return self.benchmark_data[behaviour] - - @property - def data( - self, - ) -> List: - """Returns formatted data.""" - - behavioural_data = [] - for behaviour, tool in self.benchmark_data.items(): - data = {k: v.total_time for k, v in tool.local_data.items()} - data[BenchmarkBlockTypes.TOTAL.value] = sum(data.values()) - behavioural_data.append({"behaviour": behaviour, "data": data}) - - return behavioural_data - - def save(self, period: int = 0, reset: bool = True) -> None: - """Save logs to a file.""" - - try: - self.log_dir.mkdir(exist_ok=True) - agent_dir = self.log_dir / self.context.agent_address - agent_dir.mkdir(exist_ok=True) - filepath = agent_dir / f"{period}.json" - - with open(str(filepath), "w+", encoding="utf-8") as outfile: - json.dump(self.data, outfile) - self.context.logger.debug(f"Saving benchmarking data for period: {period}") - - except PermissionError as e: # pragma: nocover - self.context.logger.error(f"Error saving benchmark data:\n{e}") - - if reset: - self.reset() - - def reset( - self, - ) -> None: - """Reset benchmark data""" - self.benchmark_data.clear() diff --git a/packages/valory/skills/abstract_round_abci/skill.yaml b/packages/valory/skills/abstract_round_abci/skill.yaml deleted file mode 100644 index 67054f0..0000000 --- a/packages/valory/skills/abstract_round_abci/skill.yaml +++ /dev/null @@ -1,164 +0,0 @@ -name: abstract_round_abci -author: valory -version: 0.1.0 -type: skill -description: abstract round-based ABCI application -license: Apache-2.0 -aea_version: '>=1.0.0, <2.0.0' -fingerprint: - README.md: bafybeievb7bhfm46p5adx3x4gvsynjpq35fcrrapzn5m2whcdt4ufxfvfq - __init__.py: bafybeihxbinbrvhj2edqthpzc2mywfzxzkf7l4v5uj6ubwnffrwgzelmre - abci_app_chain.py: bafybeibhzrixbp5x26wqhb6ogtr3af5lc4tax7lcsvk4v5rvg4psrq5yzi - base.py: bafybeihm7lf4nfqcwfvs4antge2l7eyc7vrafaw6p5canlxwy4qy4akwme - behaviour_utils.py: bafybeidhnu2ucjhlluwthpl4d6374nzmvjopy7byc2uyirajb3kswfggle - behaviours.py: bafybeifzbzy2ppabm6dpgvbsuvpgduyg7g7rei6u4ouy3nnak5top5be5u - common.py: bafybeib4coyhaxvpup7m25lsab2lpebv2wrkjp2cwihuitxmaibo6u6z2m - dialogues.py: bafybeid5sgrfa7ghnnjpssltgtey5gzt5kc2jlaitffaukvhhdbhrzcjti - handlers.py: bafybeidgby4h72qgcp3civ3c55oz3k7s4gdbkcotwhqjsbft6ylbaenjxy - io_/__init__.py: bafybeihv6ytxeo5jkbdlqjum4pfo4aaluvw4m7c55k5xncvvs7ubrlokhy - io_/ipfs.py: bafybeiffdxdt36rcwu5tyfav2umvw3hvlfjwbys3626p2g2gdlfi7djzly - io_/load.py: bafybeigkywwlsheqvd4gpyfwaxqzkkb2ih2poyicqk7e7n2mrsghxzyns4 - io_/paths.py: bafybeicfno2l4vwtmjcm3rzpp6tqi3xlkof47pypf5teecad22d44u2ple - io_/store.py: bafybeig24lslvhf7amim55ig5zzre4z45pcx3r2ozlagg3mtbr6rry2wpu - models.py: bafybeiaffpzuduwwo367cqm4uzl46mq34pdspq57o5itdb5ivyi4s743by - test_tools/__init__.py: bafybeibayeahoo73eztt2chpwi45taj2uv3dxbpyn47ksqfjoepjyaoca4 - test_tools/abci_app.py: bafybeigmrjzxfoc63xgecyngdecz4msvze4aw2iejcjewatjefjbvdlmce - test_tools/base.py: bafybeibef4lclyecne5qj4zaxnaxaqzwpxjaitqqmddgsiezduhb7pfxly - test_tools/common.py: bafybeibxlx7es632kdoeivfrjahns3kknkxfmw4rj2dcxjwqm5j6vx25sq - test_tools/integration.py: bafybeifqq3bx46hz2deph3usvrt7u45tpsapvocofd2zu3yh7rfl5nlmzq - test_tools/rounds.py: bafybeie576yxtiramzt5czpt4hnv76gfetzio2t3k5kprhdhvbpfddbaem - tests/__init__.py: bafybeie54sgqid64dyarbcttz3nnmyympyrtdyxy4lcc7c7yjxhefodbgq - tests/conftest.py: bafybeiauvmnuetxooprdsy3vlys3pha6x2rfg7acr3xrdfffr7onlmnave - tests/data/__init__.py: bafybeifmqjnrqgbau4tshhdtrosru7xyjky72ljlrf3ynrk76fxjcsgfpi - tests/data/dummy_abci/__init__.py: bafybeiaoqyjlgez5gkvutl22ihebcjk3zskve5gdt5wbap5zkmhehoddca - tests/data/dummy_abci/behaviours.py: bafybeibei4ngebbktuq6a2uvwhrulgkvn6uhaj5k3a75zihkxwnfarqh4m - tests/data/dummy_abci/dialogues.py: bafybeiaswubmqa7trhajbjn34okmpftk2sehsqrjg7znzrrd7j32xzx4vq - tests/data/dummy_abci/handlers.py: bafybeifik3ftljs63u7nm4gadxpqbcvqj53p7qftzzzfto3ioad57k3x3u - tests/data/dummy_abci/models.py: bafybeiear3i45wbaylrkbnm2fbtqorxx56glul36piuah7m7jb56f5rpoq - tests/data/dummy_abci/payloads.py: bafybeiczldqiumb7prcusb7l5vb575vschwyseyigpupvteldfyz7h6fyi - tests/data/dummy_abci/rounds.py: bafybeihhheznpcntg4z5cdd7dysnivo2g4x5biv7blriyiyoouqp6xf5aq - tests/test_abci_app_chain.py: bafybeihqvjkcwkwxowhb3umtk52us4pd5f6nbppw4ycx76oljw4j3j7xpa - tests/test_base.py: bafybeihtx2ktf6uck2l6yw72lvnvm5y224vlgawxette75cluc6juedeqe - tests/test_base_rounds.py: bafybeiadkpwuhz6y5k5ffvoqvyi6nqetf5ov5bmodejge7yvscm6yqzpse - tests/test_behaviours.py: bafybeibxxev34avddvezqumr56k7txmqkubl2c5u6y7ydjqn6kp3wabbvq - tests/test_behaviours_utils.py: bafybeidkxzhu26r2shkblz2l3syzc62uet4cxrdbschnf7vuwuuior6xkm - tests/test_common.py: bafybeiekicwjh3vu5kqppictya2bmqm3p5dcauj7cvsiunvhhultpzmyla - tests/test_dialogues.py: bafybeigpfrslqaz2yullyehia5bsl7cmy2qqxtz627ig7rbrypw5xfzeum - tests/test_handlers.py: bafybeih64lmsukci3oc5mwi636gntyx243xnbzwx64dwjxittch77qyqsu - tests/test_io/__init__.py: bafybeid3sssvbbyju4snrdssxyafleuo57sqyuepl25btxcbuj3p5oonsm - tests/test_io/test_ipfs.py: bafybeidm6f6naq6y7ntoivrqon2bkwdvd2dqru467fxqvgonv5oq5huhra - tests/test_io/test_load.py: bafybeidgnxt5rt67ackbcgi5vnlliedxakcnzgihogplolck7kp57pc6iy - tests/test_io/test_store.py: bafybeid2zbdjtgbplenacudk6re7si7dloqs2u7faqt7vhapjipjuw35ku - tests/test_models.py: bafybeicrbu6xtprfgwjs3msa3idilwqe3ymz5zx6xm326huhspzrdngrwi - tests/test_tools/__init__.py: bafybeiew6gu4pgp2sjevq4dbnmv2ail5dph7vj4yi7h3eae4gzx7vj7cbq - tests/test_tools/base.py: bafybeihi7ax53326dhin3riwwwk3bouqvsoeq26han4nspodzj6hrk3gia - tests/test_tools/test_base.py: bafybeie2hox7v6sy677grl6awq57ouliohpwhmlvrypz5rqcz5gxsxn24y - tests/test_tools/test_common.py: bafybeieauphpcqm5on7d2u2lc5lrf3esbhojp6sxlf7phrlmpqy5cfoitq - tests/test_tools/test_integration.py: bafybeidxkvb2kizi7djrpuw446dqxo2v5s7j2dbdrdpfmnd2ggezaxbnkm - tests/test_tools/test_rounds.py: bafybeibaoj4miysneipgukz7xufs47vpv5rds3ptgmu3yxlcl7gjss6ccm - tests/test_utils.py: bafybeift6igxoan2bnuexps7rrdl25jmlniqujw3odnir3cgjy4oukjjfq - utils.py: bafybeidbha3c3tcxo4lhucyx2x6yra4z2p2fp6sucqqzhxanbvgrraykbi -fingerprint_ignore_patterns: [] -connections: -- valory/abci:0.1.0:bafybeie4eixvrdpc5ifoovj24a6res6g2e22dl6di6gzib7d3fczshzyti -- valory/http_client:0.23.0:bafybeihi772xgzpqeipp3fhmvpct4y6e6tpjp4sogwqrnf3wqspgeilg4u -- valory/ipfs:0.1.0:bafybeiefkqvh5ylbk77xylcmshyuafmiecopt4gvardnubq52psvogis6a -- valory/ledger:0.19.0:bafybeihynkdraqthjtv74qk3nc5r2xubniqx2hhzpxn7bd4qmlf7q4wruq -- valory/p2p_libp2p_client:0.1.0:bafybeid3xg5k2ol5adflqloy75ibgljmol6xsvzvezebsg7oudxeeolz7e -contracts: -- valory/service_registry:0.1.0:bafybeieqgcuxmz4uxvlyb62mfsf33qy4xwa5lrij4vvcmrtcsfkng43oyq -protocols: -- open_aea/signing:1.0.0:bafybeihv62fim3wl2bayavfcg3u5e5cxu3b7brtu4cn5xoxd6lqwachasi -- valory/abci:0.1.0:bafybeiaqmp7kocbfdboksayeqhkbrynvlfzsx4uy4x6nohywnmaig4an7u -- valory/contract_api:1.0.0:bafybeidgu7o5llh26xp3u3ebq3yluull5lupiyeu6iooi2xyymdrgnzq5i -- valory/http:1.0.0:bafybeifugzl63kfdmwrxwphrnrhj7bn6iruxieme3a4ntzejf6kmtuwmae -- valory/ipfs:0.1.0:bafybeiftxi2qhreewgsc5wevogi7yc5g6hbcbo4uiuaibauhv3nhfcdtvm -- valory/ledger_api:1.0.0:bafybeihdk6psr4guxmbcrc26jr2cbgzpd5aljkqvpwo64bvaz7tdti2oni -- valory/tendermint:0.1.0:bafybeig4mi3vmlv5zpbjbfuzcgida6j5f2nhrpedxicmrrfjweqc5r7cra -skills: -- valory/abstract_abci:0.1.0:bafybeihu2bcgjk2tqjiq2zhk3uogtfszqn4osvdt7ho3fubdpdj4jgdfjm -behaviours: - main: - args: {} - class_name: AbstractRoundBehaviour -handlers: - abci: - args: {} - class_name: ABCIHandler - contract_api: - args: {} - class_name: ContractApiHandler - http: - args: {} - class_name: HttpHandler - ipfs: - args: {} - class_name: IpfsHandler - ledger_api: - args: {} - class_name: LedgerApiHandler - signing: - args: {} - class_name: SigningHandler - tendermint: - args: {} - class_name: TendermintHandler -models: - abci_dialogues: - args: {} - class_name: AbciDialogues - api_specs: - args: {} - class_name: ApiSpecs - benchmark_tool: - args: - log_dir: /logs - class_name: BenchmarkTool - contract_api_dialogues: - args: {} - class_name: ContractApiDialogues - http_dialogues: - args: {} - class_name: HttpDialogues - ipfs_dialogues: - args: {} - class_name: IpfsDialogues - ledger_api_dialogues: - args: {} - class_name: LedgerApiDialogues - requests: - args: {} - class_name: Requests - signing_dialogues: - args: {} - class_name: SigningDialogues - state: - args: {} - class_name: SharedState - tendermint_dialogues: - args: {} - class_name: TendermintDialogues -dependencies: - eth_typing: {} - hypothesis: - version: ==6.21.6 - ipfshttpclient: - version: ==0.8.0a2 - open-aea-cli-ipfs: - version: ==1.55.0 - open-aea-test-autonomy: - version: ==0.15.2 - protobuf: - version: <4.25.0,>=4.21.6 - py-ecc: - version: ==6.0.0 - pytest: - version: ==7.2.1 - pytz: - version: ==2022.2.1 - requests: - version: <2.31.2,>=2.28.1 - typing_extensions: - version: '>=3.10.0.2' -is_abstract: true -customs: [] diff --git a/packages/valory/skills/abstract_round_abci/test_tools/__init__.py b/packages/valory/skills/abstract_round_abci/test_tools/__init__.py deleted file mode 100644 index 33f9dae..0000000 --- a/packages/valory/skills/abstract_round_abci/test_tools/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Tests package for abstract_round_abci derived skills.""" # pragma: nocover diff --git a/packages/valory/skills/abstract_round_abci/test_tools/abci_app.py b/packages/valory/skills/abstract_round_abci/test_tools/abci_app.py deleted file mode 100644 index 10e1260..0000000 --- a/packages/valory/skills/abstract_round_abci/test_tools/abci_app.py +++ /dev/null @@ -1,203 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2022-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""ABCI App test tools.""" - - -from abc import ABC -from enum import Enum -from typing import Dict, Tuple, Type, Union -from unittest.mock import MagicMock - -from packages.valory.skills.abstract_round_abci.base import ( - AbciApp, - AbstractRound, - BaseSynchronizedData, - BaseTxPayload, - DegenerateRound, -) - - -class _ConcreteRound(AbstractRound, ABC): - """ConcreteRound""" - - synchronized_data_class = BaseSynchronizedData - payload_attribute = "" - - def end_block(self) -> Union[None, Tuple[MagicMock, MagicMock]]: - """End block.""" - - def check_payload(self, payload: BaseTxPayload) -> None: - """Check payload.""" - - def process_payload(self, payload: BaseTxPayload) -> None: - """Process payload.""" - - -class ConcreteRoundA(_ConcreteRound): - """Dummy instantiation of the AbstractRound class.""" - - payload_class = BaseTxPayload - - def end_block(self) -> Tuple[MagicMock, MagicMock]: - """End block.""" - return MagicMock(), MagicMock() - - -class ConcreteRoundB(_ConcreteRound): - """Dummy instantiation of the AbstractRound class.""" - - payload_class = BaseTxPayload - - -class ConcreteRoundC(_ConcreteRound): - """Dummy instantiation of the AbstractRound class.""" - - payload_class = BaseTxPayload - - -class ConcreteBackgroundRound(_ConcreteRound): - """Dummy instantiation of the AbstractRound class.""" - - payload_class = BaseTxPayload - - -class ConcreteBackgroundSlashingRound(_ConcreteRound): - """Dummy instantiation of the AbstractRound class.""" - - payload_class = BaseTxPayload - - -class ConcreteTerminationRoundA(_ConcreteRound): - """Dummy instantiation of the AbstractRound class.""" - - payload_class = BaseTxPayload - - -class ConcreteTerminationRoundB(_ConcreteRound): - """Dummy instantiation of the AbstractRound class.""" - - payload_class = BaseTxPayload - - -class ConcreteTerminationRoundC(_ConcreteRound): - """Dummy instantiation of the AbstractRound class.""" - - payload_class = BaseTxPayload - - -class ConcreteSlashingRoundA(_ConcreteRound): - """Dummy instantiation of the AbstractRound class.""" - - payload_class = BaseTxPayload - - -class ConcreteSlashingRoundB(_ConcreteRound): - """Dummy instantiation of the AbstractRound class.""" - - payload_class = BaseTxPayload - - -class ConcreteEvents(Enum): - """Defines dummy events to be used for testing purposes.""" - - TERMINATE = "terminate" - PENDING_OFFENCE = "pending_offence" - SLASH_START = "slash_start" - SLASH_END = "slash_end" - A = "a" - B = "b" - C = "c" - D = "c" - TIMEOUT = "timeout" - - def __str__(self) -> str: - """Get the string representation of the event.""" - return self.value - - -class TerminationAppTest(AbciApp[ConcreteEvents]): - """A dummy Termination abci for testing purposes.""" - - initial_round_cls: Type[AbstractRound] = ConcreteBackgroundRound - transition_function: Dict[ - Type[AbstractRound], Dict[ConcreteEvents, Type[AbstractRound]] - ] = { - ConcreteBackgroundRound: { - ConcreteEvents.TERMINATE: ConcreteTerminationRoundA, - }, - ConcreteTerminationRoundA: { - ConcreteEvents.A: ConcreteTerminationRoundA, - ConcreteEvents.B: ConcreteTerminationRoundB, - ConcreteEvents.C: ConcreteTerminationRoundC, - }, - ConcreteTerminationRoundB: { - ConcreteEvents.B: ConcreteTerminationRoundB, - ConcreteEvents.TIMEOUT: ConcreteTerminationRoundA, - }, - ConcreteTerminationRoundC: { - ConcreteEvents.C: ConcreteTerminationRoundA, - ConcreteEvents.TIMEOUT: ConcreteTerminationRoundC, - }, - } - - -class SlashingAppTest(AbciApp[ConcreteEvents]): - """A dummy Slashing abci for testing purposes.""" - - initial_round_cls: Type[AbstractRound] = ConcreteBackgroundSlashingRound - transition_function: Dict[ - Type[AbstractRound], Dict[ConcreteEvents, Type[AbstractRound]] - ] = { - ConcreteBackgroundSlashingRound: { - ConcreteEvents.SLASH_START: ConcreteSlashingRoundA, - }, - ConcreteSlashingRoundA: {ConcreteEvents.D: ConcreteSlashingRoundB}, - ConcreteSlashingRoundB: { - ConcreteEvents.SLASH_END: DegenerateRound, - }, - } - - -class AbciAppTest(AbciApp[ConcreteEvents]): - """A dummy AbciApp for testing purposes.""" - - TIMEOUT: float = 1.0 - - initial_round_cls: Type[AbstractRound] = ConcreteRoundA - transition_function: Dict[ - Type[AbstractRound], Dict[ConcreteEvents, Type[AbstractRound]] - ] = { - ConcreteRoundA: { - ConcreteEvents.A: ConcreteRoundA, - ConcreteEvents.B: ConcreteRoundB, - ConcreteEvents.C: ConcreteRoundC, - }, - ConcreteRoundB: { - ConcreteEvents.B: ConcreteRoundB, - ConcreteEvents.TIMEOUT: ConcreteRoundA, - }, - ConcreteRoundC: { - ConcreteEvents.C: ConcreteRoundA, - ConcreteEvents.TIMEOUT: ConcreteRoundC, - }, - } - event_to_timeout: Dict[ConcreteEvents, float] = { - ConcreteEvents.TIMEOUT: TIMEOUT, - } diff --git a/packages/valory/skills/abstract_round_abci/test_tools/base.py b/packages/valory/skills/abstract_round_abci/test_tools/base.py deleted file mode 100644 index 30640e9..0000000 --- a/packages/valory/skills/abstract_round_abci/test_tools/base.py +++ /dev/null @@ -1,444 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Tests for valory/abstract_round_abci skill's behaviours.""" -import json -from abc import ABC -from copy import copy -from enum import Enum -from pathlib import Path -from tempfile import TemporaryDirectory -from typing import Any, Dict, Type, cast -from unittest import mock -from unittest.mock import MagicMock - -from aea.helpers.transaction.base import SignedMessage -from aea.test_tools.test_skill import BaseSkillTestCase - -from packages.open_aea.protocols.signing import SigningMessage -from packages.valory.connections.http_client.connection import ( - PUBLIC_ID as HTTP_CLIENT_PUBLIC_ID, -) -from packages.valory.connections.ledger.connection import ( - PUBLIC_ID as LEDGER_CONNECTION_PUBLIC_ID, -) -from packages.valory.protocols.contract_api import ContractApiMessage -from packages.valory.protocols.http import HttpMessage -from packages.valory.protocols.ledger_api.message import LedgerApiMessage -from packages.valory.skills.abstract_round_abci.base import ( - AbstractRound, - BaseSynchronizedData, - BaseTxPayload, - OK_CODE, - _MetaPayload, -) -from packages.valory.skills.abstract_round_abci.behaviours import ( - AbstractRoundBehaviour, - BaseBehaviour, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - ContractApiHandler, - HttpHandler, - LedgerApiHandler, - SigningHandler, - TendermintHandler, -) - - -# pylint: disable=protected-access,too-few-public-methods,consider-using-with - - -class FSMBehaviourBaseCase(BaseSkillTestCase, ABC): - """Base case for testing FSMBehaviour classes.""" - - path_to_skill: Path - behaviour: AbstractRoundBehaviour - ledger_handler: LedgerApiHandler - http_handler: HttpHandler - contract_handler: ContractApiHandler - signing_handler: SigningHandler - tendermint_handler: TendermintHandler - old_tx_type_to_payload_cls: Dict[str, Type[BaseTxPayload]] - benchmark_dir: TemporaryDirectory - default_ledger: str = "ethereum" - - @classmethod - def setup_class(cls, **kwargs: Any) -> None: - """Setup the test class.""" - if not hasattr(cls, "path_to_skill"): - raise ValueError(f"No `path_to_skill` set on {cls}") # pragma: nocover - # works once https://github.com/valory-xyz/open-aea/issues/492 is fixed - # we need to store the current value of the meta-class attribute - # _MetaPayload.transaction_type_to_payload_cls, and restore it - # in the teardown function. We do a shallow copy so we avoid - # to modify the old mapping during the execution of the tests. - cls.old_tx_type_to_payload_cls = copy(_MetaPayload.registry) - _MetaPayload.registry = {} - super().setup_class(**kwargs) # pylint: disable=no-value-for-parameter - assert ( - cls._skill.skill_context._agent_context is not None - ), "Agent context not set" # nosec - cls._skill.skill_context._agent_context.identity._default_address_key = ( - cls.default_ledger - ) - cls._skill.skill_context._agent_context._default_ledger_id = cls.default_ledger - behaviour = cls._skill.skill_context.behaviours.main - assert isinstance( - behaviour, AbstractRoundBehaviour - ), f"{behaviour} is not of type {AbstractRoundBehaviour}" - cls.behaviour = behaviour - for attr, handler, handler_type in [ - ("http_handler", cls._skill.skill_context.handlers.http, HttpHandler), - ( - "signing_handler", - cls._skill.skill_context.handlers.signing, - SigningHandler, - ), - ( - "contract_handler", - cls._skill.skill_context.handlers.contract_api, - ContractApiHandler, - ), - ( - "ledger_handler", - cls._skill.skill_context.handlers.ledger_api, - LedgerApiHandler, - ), - ( - "tendermint_handler", - cls._skill.skill_context.handlers.tendermint, - TendermintHandler, - ), - ]: - assert isinstance( - handler, handler_type - ), f"{handler} is not of type {handler_type}" - setattr(cls, attr, handler) - - if kwargs.get("param_overrides") is not None: - for param_name, param_value in kwargs["param_overrides"].items(): - cls.behaviour.context.params.__dict__[param_name] = param_value - - def setup(self, **kwargs: Any) -> None: - """ - Set up the test method. - - Called each time before a test method is called. - - :param kwargs: the keyword arguments passed to _prepare_skill - """ - super().setup(**kwargs) - self.behaviour.setup() - self._skill.skill_context.state.setup() - self._skill.skill_context.state.round_sequence.end_sync() - - self.benchmark_dir = TemporaryDirectory() - self._skill.skill_context.benchmark_tool.__dict__["log_dir"] = Path( - self.benchmark_dir.name - ) - assert ( # nosec - cast(BaseBehaviour, self.behaviour.current_behaviour).behaviour_id - == self.behaviour.initial_behaviour_cls.auto_behaviour_id() - ) - - def fast_forward_to_behaviour( - self, - behaviour: AbstractRoundBehaviour, - behaviour_id: str, - synchronized_data: BaseSynchronizedData, - ) -> None: - """Fast forward the FSM to a behaviour.""" - next_behaviour = {s.auto_behaviour_id(): s for s in behaviour.behaviours}[ - behaviour_id - ] - next_behaviour = cast(Type[BaseBehaviour], next_behaviour) - behaviour.current_behaviour = next_behaviour( - name=next_behaviour.auto_behaviour_id(), skill_context=behaviour.context - ) - self.skill.skill_context.state.round_sequence.abci_app._round_results.append( - synchronized_data - ) - self.skill.skill_context.state.round_sequence.abci_app._extend_previous_rounds_with_current_round() - self.skill.skill_context.behaviours.main._last_round_height = ( - self.skill.skill_context.state.round_sequence.abci_app.current_round_height - ) - self.skill.skill_context.state.round_sequence.abci_app._current_round_cls = ( - next_behaviour.matching_round - ) - # consensus parameters will not be available if the current skill is abstract - consensus_params = getattr( - self.skill.skill_context.params, "consensus_params", None - ) - self.skill.skill_context.state.round_sequence.abci_app._current_round = ( - next_behaviour.matching_round(synchronized_data, consensus_params) - ) - - def mock_ledger_api_request( - self, request_kwargs: Dict, response_kwargs: Dict - ) -> None: - """ - Mock http request. - - :param request_kwargs: keyword arguments for request check. - :param response_kwargs: keyword arguments for mock response. - """ - - self.assert_quantity_in_outbox(1) - actual_ledger_api_message = self.get_message_from_outbox() - assert actual_ledger_api_message is not None, "No message in outbox." # nosec - has_attributes, error_str = self.message_has_attributes( - actual_message=actual_ledger_api_message, - message_type=LedgerApiMessage, - to=str(LEDGER_CONNECTION_PUBLIC_ID), - sender=str(self.skill.skill_context.skill_id), - **request_kwargs, - ) - - assert has_attributes, error_str # nosec - incoming_message = self.build_incoming_message( - message_type=LedgerApiMessage, - dialogue_reference=( - actual_ledger_api_message.dialogue_reference[0], - "stub", - ), - target=actual_ledger_api_message.message_id, - message_id=-1, - to=str(self.skill.skill_context.skill_id), - sender=str(LEDGER_CONNECTION_PUBLIC_ID), - ledger_id=str(LEDGER_CONNECTION_PUBLIC_ID), - **response_kwargs, - ) - self.ledger_handler.handle(incoming_message) - self.behaviour.act_wrapper() - - def mock_contract_api_request( - self, contract_id: str, request_kwargs: Dict, response_kwargs: Dict - ) -> None: - """ - Mock http request. - - :param contract_id: contract id. - :param request_kwargs: keyword arguments for request check. - :param response_kwargs: keyword arguments for mock response. - """ - - self.assert_quantity_in_outbox(1) - actual_contract_ledger_message = self.get_message_from_outbox() - assert ( # nosec - actual_contract_ledger_message is not None - ), "No message in outbox." - has_attributes, error_str = self.message_has_attributes( - actual_message=actual_contract_ledger_message, - message_type=ContractApiMessage, - to=str(LEDGER_CONNECTION_PUBLIC_ID), - sender=str(self.skill.skill_context.skill_id), - ledger_id="ethereum", - contract_id=contract_id, - message_id=1, - **request_kwargs, - ) - assert has_attributes, error_str # nosec - self.behaviour.act_wrapper() - - incoming_message = self.build_incoming_message( - message_type=ContractApiMessage, - dialogue_reference=( - actual_contract_ledger_message.dialogue_reference[0], - "stub", - ), - target=actual_contract_ledger_message.message_id, - message_id=-1, - to=str(self.skill.skill_context.skill_id), - sender=str(LEDGER_CONNECTION_PUBLIC_ID), - ledger_id="ethereum", - contract_id="mock_contract_id", - **response_kwargs, - ) - self.contract_handler.handle(incoming_message) - self.behaviour.act_wrapper() - - def mock_http_request(self, request_kwargs: Dict, response_kwargs: Dict) -> None: - """ - Mock http request. - - :param request_kwargs: keyword arguments for request check. - :param response_kwargs: keyword arguments for mock response. - """ - - self.assert_quantity_in_outbox(1) - actual_http_message = self.get_message_from_outbox() - assert actual_http_message is not None, "No message in outbox." # nosec - has_attributes, error_str = self.message_has_attributes( - actual_message=actual_http_message, - message_type=HttpMessage, - performative=HttpMessage.Performative.REQUEST, - to=str(HTTP_CLIENT_PUBLIC_ID), - sender=str(self.skill.skill_context.skill_id), - **request_kwargs, - ) - assert has_attributes, error_str # nosec - self.behaviour.act_wrapper() - self.assert_quantity_in_outbox(0) - incoming_message = self.build_incoming_message( - message_type=HttpMessage, - dialogue_reference=(actual_http_message.dialogue_reference[0], "stub"), - performative=HttpMessage.Performative.RESPONSE, - target=actual_http_message.message_id, - message_id=-1, - to=str(self.skill.skill_context.skill_id), - sender=str(HTTP_CLIENT_PUBLIC_ID), - **response_kwargs, - ) - self.http_handler.handle(incoming_message) - self.behaviour.act_wrapper() - - def mock_signing_request(self, request_kwargs: Dict, response_kwargs: Dict) -> None: - """Mock signing request.""" - self.assert_quantity_in_decision_making_queue(1) - actual_signing_message = self.get_message_from_decision_maker_inbox() - assert actual_signing_message is not None, "No message in outbox." # nosec - has_attributes, error_str = self.message_has_attributes( - actual_message=actual_signing_message, - message_type=SigningMessage, - to=self.skill.skill_context.decision_maker_address, - sender=str(self.skill.skill_context.skill_id), - **request_kwargs, - ) - assert has_attributes, error_str # nosec - incoming_message = self.build_incoming_message( - message_type=SigningMessage, - dialogue_reference=(actual_signing_message.dialogue_reference[0], "stub"), - target=actual_signing_message.message_id, - message_id=-1, - to=str(self.skill.skill_context.skill_id), - sender=self.skill.skill_context.decision_maker_address, - **response_kwargs, - ) - self.signing_handler.handle(incoming_message) - self.behaviour.act_wrapper() - - def mock_a2a_transaction( - self, - ) -> None: - """Performs mock a2a transaction.""" - - self.mock_signing_request( - request_kwargs=dict( - performative=SigningMessage.Performative.SIGN_MESSAGE, - ), - response_kwargs=dict( - performative=SigningMessage.Performative.SIGNED_MESSAGE, - signed_message=SignedMessage( - ledger_id="ethereum", body="stub_signature" - ), - ), - ) - - self.mock_http_request( - request_kwargs=dict( - method="GET", - headers="", - version="", - body=b"", - ), - response_kwargs=dict( - version="", - status_code=200, - status_text="", - headers="", - body=json.dumps({"result": {"hash": "", "code": OK_CODE}}).encode( - "utf-8" - ), - ), - ) - self.mock_http_request( - request_kwargs=dict( - method="GET", - headers="", - version="", - body=b"", - ), - response_kwargs=dict( - version="", - status_code=200, - status_text="", - headers="", - body=json.dumps({"result": {"tx_result": {"code": OK_CODE}}}).encode( - "utf-8" - ), - ), - ) - - def end_round(self, done_event: Enum) -> None: - """Ends round early to cover `wait_for_end` generator.""" - current_behaviour = cast(BaseBehaviour, self.behaviour.current_behaviour) - if current_behaviour is None: - return - current_behaviour = cast(BaseBehaviour, current_behaviour) - abci_app = current_behaviour.context.state.round_sequence.abci_app - old_round = abci_app._current_round - abci_app._last_round = old_round - abci_app._current_round = abci_app.transition_function[ - current_behaviour.matching_round - ][done_event](abci_app.synchronized_data, context=MagicMock()) - abci_app._previous_rounds.append(old_round) - abci_app._current_round_height += 1 - self.behaviour._process_current_round() - - def _test_done_flag_set(self) -> None: - """Test that, when round ends, the 'done' flag is set.""" - current_behaviour = cast(BaseBehaviour, self.behaviour.current_behaviour) - assert not current_behaviour.is_done() # nosec - with mock.patch.object( - self.behaviour.context.state, "_round_sequence" - ) as mock_round_sequence: - mock_round_sequence.last_round_id = cast( - AbstractRound, current_behaviour.matching_round - ).auto_round_id() - current_behaviour.act_wrapper() - assert current_behaviour.is_done() # nosec - - @classmethod - def teardown_class(cls) -> None: - """Teardown the test class.""" - if getattr(cls, "old_tx_type_to_payload_cls", False): - _MetaPayload.registry = cls.old_tx_type_to_payload_cls - - def teardown(self, **kwargs: Any) -> None: - """Teardown.""" - super().teardown(**kwargs) - self.benchmark_dir.cleanup() - - -class DummyContext: - """Dummy Context class for testing shared state initialization.""" - - class params: - """Dummy param variable.""" - - round_timeout_seconds: float = 1.0 - - _skill: MagicMock = MagicMock() - logger: MagicMock = MagicMock() - skill_id = "dummy_skill_id" - - @property - def is_abstract_component(self) -> bool: - """Mock for is_abstract.""" - return True diff --git a/packages/valory/skills/abstract_round_abci/test_tools/common.py b/packages/valory/skills/abstract_round_abci/test_tools/common.py deleted file mode 100644 index 8fdab26..0000000 --- a/packages/valory/skills/abstract_round_abci/test_tools/common.py +++ /dev/null @@ -1,432 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2022-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test common classes.""" -import binascii -import json -import time -from pathlib import Path -from typing import Any, Set, Type, cast -from unittest import mock - -import pytest -from aea.exceptions import AEAActException -from aea.skills.base import SkillContext - -from packages.valory.protocols.contract_api.custom_types import State -from packages.valory.protocols.ledger_api.message import LedgerApiMessage -from packages.valory.skills.abstract_round_abci.base import ( - AbciAppDB, - BaseSynchronizedData, -) -from packages.valory.skills.abstract_round_abci.behaviour_utils import BaseBehaviour -from packages.valory.skills.abstract_round_abci.test_tools.base import ( - FSMBehaviourBaseCase, -) - - -PACKAGE_DIR = Path(__file__).parent.parent -DRAND_VALUE = { - "round": 1416669, - "randomness": "f6be4bf1fa229f22340c1a5b258f809ac4af558200775a67dacb05f0cb258a11", - "signature": ( - "b44d00516f46da3a503f9559a634869b6dc2e5d839e46ec61a090e3032172954929a5" - "d9bd7197d7739fe55db770543c71182562bd0ad20922eb4fe6b8a1062ed21df3b68de" - "44694eb4f20b35262fa9d63aa80ad3f6172dd4d33a663f21179604" - ), - "previous_signature": ( - "903c60a4b937a804001032499a855025573040cb86017c38e2b1c3725286756ce8f33" - "61188789c17336beaf3f9dbf84b0ad3c86add187987a9a0685bc5a303e37b008fba8c" - "44f02a416480dd117a3ff8b8075b1b7362c58af195573623187463" - ), -} - - -class CommonBaseCase(FSMBehaviourBaseCase): - """Base case for testing PriceEstimation FSMBehaviour.""" - - path_to_skill = PACKAGE_DIR = Path(__file__).parent.parent - - -class BaseRandomnessBehaviourTest(CommonBaseCase): - """Test RandomnessBehaviour.""" - - randomness_behaviour_class: Type[BaseBehaviour] - next_behaviour_class: Type[BaseBehaviour] - done_event: Any - - def test_randomness_behaviour( - self, - ) -> None: - """Test RandomnessBehaviour.""" - - self.fast_forward_to_behaviour( - self.behaviour, - self.randomness_behaviour_class.auto_behaviour_id(), - BaseSynchronizedData(AbciAppDB(setup_data={})), - ) - assert ( - cast( - BaseBehaviour, - cast(BaseBehaviour, self.behaviour.current_behaviour), - ).behaviour_id - == self.randomness_behaviour_class.auto_behaviour_id() - ) - self.behaviour.act_wrapper() - self.mock_http_request( - request_kwargs=dict( - method="GET", - headers="", - version="", - body=b"", - url="https://drand.cloudflare.com/public/latest", - ), - response_kwargs=dict( - version="", - status_code=200, - status_text="", - headers="", - body=json.dumps(DRAND_VALUE).encode("utf-8"), - ), - ) - - self.behaviour.act_wrapper() - self.mock_a2a_transaction() - self._test_done_flag_set() - self.end_round(self.done_event) - - behaviour = cast(BaseBehaviour, self.behaviour.current_behaviour) - assert behaviour.behaviour_id == self.next_behaviour_class.auto_behaviour_id() - - def test_invalid_drand_value( - self, - ) -> None: - """Test invalid drand values.""" - self.fast_forward_to_behaviour( - self.behaviour, - self.randomness_behaviour_class.auto_behaviour_id(), - BaseSynchronizedData(AbciAppDB(setup_data={})), - ) - assert ( - cast( - BaseBehaviour, - cast(BaseBehaviour, self.behaviour.current_behaviour), - ).behaviour_id - == self.randomness_behaviour_class.auto_behaviour_id() - ) - self.behaviour.act_wrapper() - - drand_value = DRAND_VALUE.copy() - drand_value["randomness"] = binascii.hexlify(b"randomness_hex").decode() - self.mock_http_request( - request_kwargs=dict( - method="GET", - headers="", - version="", - body=b"", - url="https://drand.cloudflare.com/public/latest", - ), - response_kwargs=dict( - version="", - status_code=200, - status_text="", - headers="", - body=json.dumps(drand_value).encode(), - ), - ) - - def test_invalid_response( - self, - ) -> None: - """Test invalid json response.""" - self.fast_forward_to_behaviour( - self.behaviour, - self.randomness_behaviour_class.auto_behaviour_id(), - BaseSynchronizedData(AbciAppDB(setup_data={})), - ) - assert ( - cast( - BaseBehaviour, - cast(BaseBehaviour, self.behaviour.current_behaviour), - ).behaviour_id - == self.randomness_behaviour_class.auto_behaviour_id() - ) - self.behaviour.act_wrapper() - - self.mock_http_request( - request_kwargs=dict( - method="GET", - headers="", - version="", - body=b"", - url="https://drand.cloudflare.com/public/latest", - ), - response_kwargs=dict( - version="", status_code=200, status_text="", headers="", body=b"" - ), - ) - self.behaviour.act_wrapper() - time.sleep(1) - self.behaviour.act_wrapper() - - def test_max_retries_reached_fallback( - self, - ) -> None: - """Test with max retries reached.""" - self.fast_forward_to_behaviour( - self.behaviour, - self.randomness_behaviour_class.auto_behaviour_id(), - BaseSynchronizedData(AbciAppDB(setup_data={})), - ) - assert ( - cast( - BaseBehaviour, - cast(BaseBehaviour, self.behaviour.current_behaviour), - ).behaviour_id - == self.randomness_behaviour_class.auto_behaviour_id() - ) - self.behaviour.context.randomness_api.__dict__["_frozen"] = False - with mock.patch.object( - self.behaviour.context.randomness_api, - "is_retries_exceeded", - return_value=True, - ): - self.behaviour.act_wrapper() - self.mock_ledger_api_request( - request_kwargs=dict( - performative=LedgerApiMessage.Performative.GET_STATE - ), - response_kwargs=dict( - performative=LedgerApiMessage.Performative.STATE, - state=State(ledger_id="ethereum", body={"hash": "0xa"}), - ), - ) - - self.behaviour.act_wrapper() - self.mock_a2a_transaction() - self._test_done_flag_set() - self.end_round(self.done_event) - - behaviour = cast(BaseBehaviour, self.behaviour.current_behaviour) - assert ( - behaviour.behaviour_id == self.next_behaviour_class.auto_behaviour_id() - ) - self.behaviour.context.randomness_api.__dict__["_frozen"] = True - - def test_max_retries_reached_fallback_fail( - self, - ) -> None: - """Test with max retries reached.""" - self.fast_forward_to_behaviour( - self.behaviour, - self.randomness_behaviour_class.auto_behaviour_id(), - BaseSynchronizedData(AbciAppDB(setup_data={})), - ) - assert ( - cast( - BaseBehaviour, - cast(BaseBehaviour, self.behaviour.current_behaviour), - ).behaviour_id - == self.randomness_behaviour_class.auto_behaviour_id() - ) - self.behaviour.context.randomness_api.__dict__["_frozen"] = False - with mock.patch.object( - self.behaviour.context.randomness_api, - "is_retries_exceeded", - return_value=True, - ): - self.behaviour.act_wrapper() - self.mock_ledger_api_request( - request_kwargs=dict( - performative=LedgerApiMessage.Performative.GET_STATE - ), - response_kwargs=dict( - performative=LedgerApiMessage.Performative.ERROR, - state=State(ledger_id="ethereum", body={}), - ), - ) - - self.behaviour.act_wrapper() - self.behaviour.context.randomness_api.__dict__["_frozen"] = True - - def test_max_retries_reached_fallback_fail_case_2( - self, - ) -> None: - """Test with max retries reached.""" - self.fast_forward_to_behaviour( - self.behaviour, - self.randomness_behaviour_class.auto_behaviour_id(), - BaseSynchronizedData(AbciAppDB(setup_data={})), - ) - assert ( - cast( - BaseBehaviour, - cast(BaseBehaviour, self.behaviour.current_behaviour), - ).behaviour_id - == self.randomness_behaviour_class.auto_behaviour_id() - ) - self.behaviour.context.randomness_api.__dict__["_frozen"] = False - with mock.patch.object( - self.behaviour.context.randomness_api, - "is_retries_exceeded", - return_value=True, - ): - self.behaviour.act_wrapper() - self.mock_ledger_api_request( - request_kwargs=dict( - performative=LedgerApiMessage.Performative.GET_STATE - ), - response_kwargs=dict( - performative=LedgerApiMessage.Performative.STATE, - state=State(ledger_id="ethereum", body={}), - ), - ) - - self.behaviour.act_wrapper() - self.behaviour.context.randomness_api.__dict__["_frozen"] = True - - def test_clean_up( - self, - ) -> None: - """Test when `observed` value is none.""" - self.fast_forward_to_behaviour( - self.behaviour, - self.randomness_behaviour_class.auto_behaviour_id(), - BaseSynchronizedData(AbciAppDB(setup_data={})), - ) - assert ( - cast( - BaseBehaviour, - cast(BaseBehaviour, self.behaviour.current_behaviour), - ).behaviour_id - == self.randomness_behaviour_class.auto_behaviour_id() - ) - self.behaviour.context.randomness_api.retries_info.retries_attempted = ( # pylint: disable=protected-access - 1 - ) - assert self.behaviour.current_behaviour is not None - self.behaviour.current_behaviour.clean_up() - assert ( - self.behaviour.context.randomness_api.retries_info.retries_attempted # pylint: disable=protected-access - == 0 - ) - - -class BaseSelectKeeperBehaviourTest(CommonBaseCase): - """Test SelectKeeperBehaviour.""" - - select_keeper_behaviour_class: Type[BaseBehaviour] - next_behaviour_class: Type[BaseBehaviour] - done_event: Any - _synchronized_data: Type[BaseSynchronizedData] = BaseSynchronizedData - - @mock.patch.object(SkillContext, "agent_address", new_callable=mock.PropertyMock) - @pytest.mark.parametrize( - "blacklisted_keepers", - ( - set(), - {"a_1"}, - {"test_agent_address" + "t" * 24}, - {"a_1" + "t" * 39, "a_2" + "t" * 39, "test_agent_address" + "t" * 24}, - ), - ) - def test_select_keeper( - self, agent_address_mock: mock.Mock, blacklisted_keepers: Set[str] - ) -> None: - """Test select keeper agent.""" - agent_address_mock.return_value = "test_agent_address" + "t" * 24 - participants = ( - self.skill.skill_context.agent_address, - "a_1" + "t" * 39, - "a_2" + "t" * 39, - ) - self.fast_forward_to_behaviour( - behaviour=self.behaviour, - behaviour_id=self.select_keeper_behaviour_class.auto_behaviour_id(), - synchronized_data=self._synchronized_data( - AbciAppDB( - setup_data=AbciAppDB.data_to_lists( - dict( - participants=participants, - most_voted_randomness="56cbde9e9bbcbdcaf92f183c678eaa5288581f06b1c9c7f884ce911776727688", - blacklisted_keepers="".join(blacklisted_keepers), - ) - ), - ) - ), - ) - assert self.behaviour.current_behaviour is not None - assert ( - self.behaviour.current_behaviour.behaviour_id - == self.select_keeper_behaviour_class.auto_behaviour_id() - ) - - if ( - self.behaviour.current_behaviour.synchronized_data.participants - - self.behaviour.current_behaviour.synchronized_data.blacklisted_keepers - ): - self.behaviour.act_wrapper() - self.mock_a2a_transaction() - self._test_done_flag_set() - self.end_round(self.done_event) - assert ( - self.behaviour.current_behaviour.behaviour_id - == self.next_behaviour_class.auto_behaviour_id() - ) - else: - with pytest.raises( - AEAActException, - match="Cannot continue if all the keepers have been blacklisted!", - ): - self.behaviour.act_wrapper() - - def test_select_keeper_preexisting_keeper( - self, - ) -> None: - """Test select keeper agent.""" - participants = (self.skill.skill_context.agent_address, "a_1", "a_2") - preexisting_keeper = next(iter(participants)) - self.fast_forward_to_behaviour( - behaviour=self.behaviour, - behaviour_id=self.select_keeper_behaviour_class.auto_behaviour_id(), - synchronized_data=self._synchronized_data( - AbciAppDB( - setup_data=dict( - participants=[participants], - most_voted_randomness=[ - "56cbde9e9bbcbdcaf92f183c678eaa5288581f06b1c9c7f884ce911776727688" - ], - most_voted_keeper_address=[preexisting_keeper], - ), - ) - ), - ) - assert ( - cast( - BaseBehaviour, - cast(BaseBehaviour, self.behaviour.current_behaviour), - ).behaviour_id - == self.select_keeper_behaviour_class.auto_behaviour_id() - ) - self.behaviour.act_wrapper() - self.mock_a2a_transaction() - self._test_done_flag_set() - self.end_round(self.done_event) - behaviour = cast(BaseBehaviour, self.behaviour.current_behaviour) - assert behaviour.behaviour_id == self.next_behaviour_class.auto_behaviour_id() diff --git a/packages/valory/skills/abstract_round_abci/test_tools/integration.py b/packages/valory/skills/abstract_round_abci/test_tools/integration.py deleted file mode 100644 index 9134139..0000000 --- a/packages/valory/skills/abstract_round_abci/test_tools/integration.py +++ /dev/null @@ -1,301 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Integration tests for various transaction settlement skill's failure modes.""" - - -import asyncio -import os -import tempfile -import time -from abc import ABC -from pathlib import Path -from threading import Thread -from typing import Any, Callable, Dict, List, Optional, Tuple, cast - -from aea.crypto.wallet import Wallet -from aea.decision_maker.base import DecisionMaker -from aea.decision_maker.default import DecisionMakerHandler -from aea.identity.base import Identity -from aea.mail.base import Envelope -from aea.multiplexer import Multiplexer -from aea.protocols.base import Address, Message -from aea.skills.base import Handler -from web3 import HTTPProvider, Web3 -from web3.providers import BaseProvider - -from packages.valory.skills.abstract_round_abci.base import BaseSynchronizedData -from packages.valory.skills.abstract_round_abci.behaviour_utils import BaseBehaviour -from packages.valory.skills.abstract_round_abci.handlers import SigningHandler -from packages.valory.skills.abstract_round_abci.test_tools.base import ( - FSMBehaviourBaseCase, -) - - -# pylint: disable=protected-access,too-many-ancestors,unbalanced-tuple-unpacking,too-many-locals,consider-using-with,unspecified-encoding,too-many-arguments,unidiomatic-typecheck - -HandlersType = List[Optional[Handler]] -ExpectedContentType = List[ - Optional[ - Dict[ - str, - Any, - ] - ] -] -ExpectedTypesType = List[ - Optional[ - Dict[ - str, - Any, - ] - ] -] - - -class IntegrationBaseCase(FSMBehaviourBaseCase, ABC): - """Base test class for integration tests.""" - - running_loop: asyncio.AbstractEventLoop - thread_loop: Thread - multiplexer: Multiplexer - decision_maker: DecisionMaker - agents: Dict[str, Address] = { - "0xBcd4042DE499D14e55001CcbB24a551F3b954096": "0xf214f2b2cd398c806f84e317254e0f0b801d0643303237d97a22a48e01628897", - "0x71bE63f3384f5fb98995898A86B02Fb2426c5788": "0x701b615bbdfb9de65240bc28bd21bbc0d996645a3dd57e7b12bc2bdf6f192c82", - "0xFABB0ac9d68B0B445fB7357272Ff202C5651694a": "0xa267530f49f8280200edf313ee7af6b827f2a8bce2897751d06a843f644967b1", - "0x1CBd3b2770909D4e10f157cABC84C7264073C9Ec": "0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd", - } - current_agent: Address - ROOT_DIR: Path - make_ledger_api_connection_callable: Callable - - @classmethod - def _setup_class(cls, **kwargs: Any) -> None: - """Setup class.""" - - @classmethod - def setup_class(cls, **kwargs: Any) -> None: - """Setup.""" - super().setup_class() - - # set up a multiplexer with the required connections - cls.running_loop = asyncio.new_event_loop() - cls.thread_loop = Thread(target=cls.running_loop.run_forever) - cls.thread_loop.start() - cls.multiplexer = Multiplexer( - [cls.make_ledger_api_connection_callable()], loop=cls.running_loop - ) - cls.multiplexer.connect() - - # hardhat configuration - # setup decision maker - with tempfile.TemporaryDirectory() as temp_dir: - fp = os.path.join(temp_dir, "key.txt") - f = open(fp, "w") - f.write(cls.agents[next(iter(cls.agents))]) - f.close() - wallet = Wallet(private_key_paths={"ethereum": str(fp)}) - identity = Identity( - "test_agent_name", - addresses=wallet.addresses, - public_keys=wallet.public_keys, - default_address_key="ethereum", - ) - cls._skill._skill_context._agent_context._identity = identity - cls.current_agent = identity.address - - cls.decision_maker = DecisionMaker( - decision_maker_handler=DecisionMakerHandler(identity, wallet, {}) - ) - cls._skill._skill_context._agent_context._decision_maker_message_queue = ( - cls.decision_maker.message_in_queue - ) - cls._skill.skill_context._agent_context._decision_maker_address = ( - "decision_maker" - ) - - @classmethod - def teardown_class(cls) -> None: - """Tear down the multiplexer.""" - cls.multiplexer.disconnect() - cls.running_loop.call_soon_threadsafe(cls.running_loop.stop) - cls.thread_loop.join() - super().teardown_class() - - def get_message_from_decision_maker_inbox(self) -> Optional[Message]: - """Get message from decision maker inbox.""" - if self._skill.skill_context.decision_maker_message_queue.empty(): - return None - return self._skill.skill_context.decision_maker_message_queue.protected_get( - self.decision_maker._queue_access_code, block=True - ) - - def process_message_cycle( - self, - handler: Optional[Handler] = None, - expected_content: Optional[Dict] = None, - expected_types: Optional[Dict] = None, - mining_interval_secs: float = 0, - ) -> Optional[Message]: - """ - Processes one request-response type message cycle. - - Steps: - 1. Calls act on behaviour to generate outgoing message - 2. Checks for message in outbox - 3. Sends message to multiplexer and waits for response. - 4. Passes message to handler - 5. Calls act on behaviour to process incoming message - - :param handler: the handler to handle a potential incoming message - :param expected_content: the content to be expected - :param expected_types: the types to be expected - :param mining_interval_secs: the mining interval used in the tests - :return: the incoming message - """ - if expected_types and tuple(expected_types)[0] == "transaction_receipt": - time.sleep(mining_interval_secs) # pragma: no cover - self.behaviour.act_wrapper() - incoming_message = None - - if type(handler) == SigningHandler: - self.assert_quantity_in_decision_making_queue(1) - message = self.get_message_from_decision_maker_inbox() - assert message is not None, "No message in outbox." # nosec - self.decision_maker.handle(message) - if handler is not None: - incoming_message = self.decision_maker.message_out_queue.get(block=True) - assert isinstance(incoming_message, Message) # nosec - else: - self.assert_quantity_in_outbox(1) - message = self.get_message_from_outbox() - assert message is not None, "No message in outbox." # nosec - self.multiplexer.put( - Envelope( - to=message.to, - sender=message.sender, - message=message, - context=None, - ) - ) - if handler is not None: - envelope = self.multiplexer.get(block=True) - assert envelope is not None, "No envelope" # nosec - incoming_message = envelope.message - assert isinstance(incoming_message, Message) # nosec - - if handler is not None: - assert incoming_message is not None # nosec - if expected_content is not None: - assert all( # nosec - [ - incoming_message._body.get(key, None) == value - for key, value in expected_content.items() - ] - ), f"Actual content: {incoming_message._body}, expected: {expected_content}" - - if expected_types is not None: - assert all( # nosec - [ - type(incoming_message._body.get(key, None)) == value_type - for key, value_type in expected_types.items() - ] - ), "Content type mismatch" - handler.handle(incoming_message) - return incoming_message - return None - - def process_n_messages( - self, - ncycles: int, - synchronized_data: Optional[BaseSynchronizedData] = None, - behaviour_id: Optional[str] = None, - handlers: Optional[HandlersType] = None, - expected_content: Optional[ExpectedContentType] = None, - expected_types: Optional[ExpectedTypesType] = None, - fail_send_a2a: bool = False, - mining_interval_secs: float = 0, - ) -> Tuple[Optional[Message], ...]: - """ - Process n message cycles. - - :param behaviour_id: the behaviour to fast forward to - :param ncycles: the number of message cycles to process - :param synchronized_data: a synchronized_data - :param handlers: a list of handlers - :param expected_content: the expected_content - :param expected_types: the expected type - :param fail_send_a2a: flag that indicates whether we want to simulate a failure in the `send_a2a_transaction` - :param mining_interval_secs: the mining interval used in the tests. - - :return: tuple of incoming messages - """ - handlers = [None] * ncycles if handlers is None else handlers - expected_content = ( - [None] * ncycles if expected_content is None else expected_content - ) - expected_types = [None] * ncycles if expected_types is None else expected_types - assert ( # nosec - len(expected_content) == len(expected_types) - and len(expected_content) == len(handlers) - and len(expected_content) == ncycles - ), "Number of cycles, handlers, contents and types does not match" - - if behaviour_id is not None and synchronized_data is not None: - self.fast_forward_to_behaviour( - behaviour=self.behaviour, - behaviour_id=behaviour_id, - synchronized_data=synchronized_data, - ) - assert ( # nosec - cast(BaseBehaviour, self.behaviour.current_behaviour).behaviour_id - == behaviour_id - ) - - incoming_messages = [] - for i in range(ncycles): - incoming_message = self.process_message_cycle( - handlers[i], - expected_content[i], - expected_types[i], - mining_interval_secs, - ) - incoming_messages.append(incoming_message) - - self.behaviour.act_wrapper() - if not fail_send_a2a: - self.mock_a2a_transaction() - return tuple(incoming_messages) - - -class HardHatHelperIntegration(IntegrationBaseCase, ABC): # pragma: no cover - """Base test class for integration tests with HardHat provider.""" - - hardhat_provider: BaseProvider - - @classmethod - def setup_class(cls, **kwargs: Any) -> None: - """Setup.""" - super().setup_class() - - # create an API for HardHat - cls.hardhat_provider = Web3( - provider=HTTPProvider("http://localhost:8545") - ).provider diff --git a/packages/valory/skills/abstract_round_abci/test_tools/rounds.py b/packages/valory/skills/abstract_round_abci/test_tools/rounds.py deleted file mode 100644 index 77f50c8..0000000 --- a/packages/valory/skills/abstract_round_abci/test_tools/rounds.py +++ /dev/null @@ -1,599 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2022-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test tools for testing rounds.""" - -import re -from copy import deepcopy -from dataclasses import dataclass -from enum import Enum -from typing import ( - Any, - Callable, - FrozenSet, - Generator, - List, - Mapping, - Optional, - Tuple, - Type, -) -from unittest import mock - -import pytest - -from packages.valory.skills.abstract_round_abci.base import ( - ABCIAppInternalError, - AbciAppDB, - AbstractRound, - BaseSynchronizedData, - BaseTxPayload, - CollectDifferentUntilAllRound, - CollectDifferentUntilThresholdRound, - CollectNonEmptyUntilThresholdRound, - CollectSameUntilAllRound, - CollectSameUntilThresholdRound, - CollectionRound, - OnlyKeeperSendsRound, - TransactionNotValidError, - VotingRound, -) - - -MAX_PARTICIPANTS: int = 4 - - -def get_participants() -> FrozenSet[str]: - """Participants""" - return frozenset([f"agent_{i}" for i in range(MAX_PARTICIPANTS)]) - - -class DummyEvent(Enum): - """Dummy Event""" - - DONE = "done" - ROUND_TIMEOUT = "round_timeout" - NO_MAJORITY = "no_majority" - RESET_TIMEOUT = "reset_timeout" - NEGATIVE = "negative" - NONE = "none" - FAIL = "fail" - - -@dataclass(frozen=True) -class DummyTxPayload(BaseTxPayload): - """Dummy Transaction Payload.""" - - value: Optional[str] = None - vote: Optional[bool] = None - - -class DummySynchronizedData(BaseSynchronizedData): - """Dummy synchronized data for tests.""" - - -def get_dummy_tx_payloads( - participants: FrozenSet[str], - value: Any = None, - vote: Optional[bool] = False, - is_value_none: bool = False, - is_vote_none: bool = False, -) -> List[DummyTxPayload]: - """Returns a list of DummyTxPayload objects.""" - return [ - DummyTxPayload( - sender=agent, - value=(value or agent) if not is_value_none else value, - vote=vote if not is_vote_none else None, - ) - for agent in sorted(participants) - ] - - -class DummyRound(AbstractRound): - """Dummy round.""" - - payload_class = DummyTxPayload - payload_attribute = "value" - synchronized_data_class = BaseSynchronizedData - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: - """end_block method.""" - - -class DummyCollectionRound(CollectionRound, DummyRound): - """Dummy Class for CollectionRound""" - - -class DummyCollectDifferentUntilAllRound(CollectDifferentUntilAllRound, DummyRound): - """Dummy Class for CollectDifferentUntilAllRound""" - - -class DummyCollectSameUntilAllRound(CollectSameUntilAllRound, DummyRound): - """Dummy Class for CollectSameUntilThresholdRound""" - - -class DummyCollectDifferentUntilThresholdRound( - CollectDifferentUntilThresholdRound, DummyRound -): - """Dummy Class for CollectDifferentUntilThresholdRound""" - - -class DummyCollectSameUntilThresholdRound(CollectSameUntilThresholdRound, DummyRound): - """Dummy Class for CollectSameUntilThresholdRound""" - - -class DummyOnlyKeeperSendsRound(OnlyKeeperSendsRound, DummyRound): - """Dummy Class for OnlyKeeperSendsRound""" - - fail_event = "FAIL_EVENT" - - -class DummyVotingRound(VotingRound, DummyRound): - """Dummy Class for VotingRound""" - - -class DummyCollectNonEmptyUntilThresholdRound( - CollectNonEmptyUntilThresholdRound, DummyRound -): - """Dummy Class for `CollectNonEmptyUntilThresholdRound`""" - - -class BaseRoundTestClass: # pylint: disable=too-few-public-methods - """Base test class.""" - - synchronized_data: BaseSynchronizedData - participants: FrozenSet[str] - - _synchronized_data_class: Type[BaseSynchronizedData] - _event_class: Any - - def setup( - self, - ) -> None: - """Setup test class.""" - - self.participants = get_participants() - self.synchronized_data = self._synchronized_data_class( - db=AbciAppDB( - setup_data=dict( - participants=[tuple(self.participants)], - all_participants=[tuple(self.participants)], - consensus_threshold=[3], - safe_contract_address=["test_address"], - ), - ) - ) - - def _test_no_majority_event(self, round_obj: AbstractRound) -> None: - """Test the NO_MAJORITY event.""" - with mock.patch.object(round_obj, "is_majority_possible", return_value=False): - result = round_obj.end_block() - assert result is not None - _, event = result - assert event == self._event_class.NO_MAJORITY - - @staticmethod - def _complete_run( - test_runner: Generator, iter_count: int = MAX_PARTICIPANTS - ) -> None: - """ - This method represents logic to execute test logic defined in _test_round method. - - _test_round should follow these steps - - 1. process first payload - 2. yield test_round - 3. test collection, end_block and thresholds - 4. process rest of the payloads - 5. yield test_round - 6. yield synchronized_data, event ( returned from end_block ) - 7. test synchronized_data and event - - :param test_runner: test runner - :param iter_count: iter_count - """ - - for _ in range(iter_count): - next(test_runner) - - -class BaseCollectDifferentUntilAllRoundTest( # pylint: disable=too-few-public-methods - BaseRoundTestClass -): - """Tests for rounds derived from CollectDifferentUntilAllRound.""" - - def _test_round( - self, - test_round: CollectDifferentUntilAllRound, - round_payloads: List[BaseTxPayload], - synchronized_data_update_fn: Callable, - synchronized_data_attr_checks: List[Callable], - exit_event: Any, - ) -> Generator: - """Test round.""" - - first_payload = round_payloads.pop(0) - test_round.process_payload(first_payload) - - yield test_round - assert test_round.collection[first_payload.sender] == first_payload - assert not test_round.collection_threshold_reached - assert test_round.end_block() is None - - for payload in round_payloads: - test_round.process_payload(payload) - yield test_round - assert test_round.collection_threshold_reached - - actual_next_synchronized_data = synchronized_data_update_fn( - deepcopy(self.synchronized_data), test_round - ) - - res = test_round.end_block() - yield res - if exit_event is None: - assert res is exit_event - else: - assert res is not None - synchronized_data, event = res - for behaviour_attr_getter in synchronized_data_attr_checks: - assert behaviour_attr_getter( - synchronized_data - ) == behaviour_attr_getter(actual_next_synchronized_data) - assert event == exit_event - yield - - -class BaseCollectSameUntilAllRoundTest( - BaseRoundTestClass -): # pylint: disable=too-few-public-methods - """Tests for rounds derived from CollectSameUntilAllRound.""" - - def _test_round( # pylint: disable=too-many-arguments,too-many-locals - self, - test_round: CollectSameUntilAllRound, - round_payloads: Mapping[str, BaseTxPayload], - synchronized_data_update_fn: Callable, - synchronized_data_attr_checks: List[Callable], - most_voted_payload: Any, - exit_event: Any, - finished: bool, - ) -> Generator: - """Test rounds derived from CollectionRound.""" - - (_, first_payload), *payloads = round_payloads.items() - - test_round.process_payload(first_payload) - yield test_round - assert test_round.collection[first_payload.sender] == first_payload - assert not test_round.collection_threshold_reached - assert test_round.end_block() is None - - with pytest.raises( - ABCIAppInternalError, - match="internal error: 1 votes are not enough for `CollectSameUntilAllRound`. " - f"Expected: `n_votes = max_participants = {MAX_PARTICIPANTS}`", - ): - _ = test_round.common_payload - - for _, payload in payloads: - test_round.process_payload(payload) - yield test_round - if finished: - assert test_round.collection_threshold_reached - assert test_round.common_payload == most_voted_payload - - actual_next_synchronized_data = synchronized_data_update_fn( - deepcopy(self.synchronized_data), test_round - ) - res = test_round.end_block() - yield res - assert res is not None - - synchronized_data, event = res - - for behaviour_attr_getter in synchronized_data_attr_checks: - assert behaviour_attr_getter(synchronized_data) == behaviour_attr_getter( - actual_next_synchronized_data - ), f"Mismatch in synchronized_data. Actual:\n{behaviour_attr_getter(synchronized_data)}\nExpected:\n{behaviour_attr_getter(actual_next_synchronized_data)}" - assert event == exit_event - yield - - -class BaseCollectSameUntilThresholdRoundTest( # pylint: disable=too-few-public-methods - BaseRoundTestClass -): - """Tests for rounds derived from CollectSameUntilThresholdRound.""" - - def _test_round( # pylint: disable=too-many-arguments,too-many-locals - self, - test_round: CollectSameUntilThresholdRound, - round_payloads: Mapping[str, BaseTxPayload], - synchronized_data_update_fn: Callable, - synchronized_data_attr_checks: List[Callable], - most_voted_payload: Any, - exit_event: Any, - ) -> Generator: - """Test rounds derived from CollectionRound.""" - - (_, first_payload), *payloads = round_payloads.items() - - test_round.process_payload(first_payload) - yield test_round - assert test_round.collection[first_payload.sender] == first_payload - assert not test_round.threshold_reached - assert test_round.end_block() is None - - self._test_no_majority_event(test_round) - with pytest.raises(ABCIAppInternalError, match="not enough votes"): - _ = test_round.most_voted_payload - - for _, payload in payloads: - test_round.process_payload(payload) - yield test_round - assert test_round.threshold_reached - assert test_round.most_voted_payload == most_voted_payload - - actual_next_synchronized_data = synchronized_data_update_fn( - deepcopy(self.synchronized_data), test_round - ) - res = test_round.end_block() - yield res - assert res is not None - - synchronized_data, event = res - - for behaviour_attr_getter in synchronized_data_attr_checks: - assert behaviour_attr_getter(synchronized_data) == behaviour_attr_getter( - actual_next_synchronized_data - ), f"Mismatch in synchronized_data. Actual:\n{behaviour_attr_getter(synchronized_data)}\nExpected:\n{behaviour_attr_getter(actual_next_synchronized_data)}" - assert event == exit_event - yield - - -class BaseOnlyKeeperSendsRoundTest( # pylint: disable=too-few-public-methods - BaseRoundTestClass -): - """Tests for rounds derived from OnlyKeeperSendsRound.""" - - def _test_round( - self, - test_round: OnlyKeeperSendsRound, - keeper_payloads: BaseTxPayload, - synchronized_data_update_fn: Callable, - synchronized_data_attr_checks: List[Callable], - exit_event: Any, - ) -> Generator: - """Test for rounds derived from OnlyKeeperSendsRound.""" - - assert test_round.end_block() is None - assert test_round.keeper_payload is None - - test_round.process_payload(keeper_payloads) - yield test_round - assert test_round.keeper_payload is not None - - yield test_round - actual_next_synchronized_data = synchronized_data_update_fn( - deepcopy(self.synchronized_data), test_round - ) - res = test_round.end_block() - yield res - assert res is not None - - synchronized_data, event = res - - for behaviour_attr_getter in synchronized_data_attr_checks: - assert behaviour_attr_getter(synchronized_data) == behaviour_attr_getter( - actual_next_synchronized_data - ), f"Mismatch in synchronized_data. Actual:\n{behaviour_attr_getter(synchronized_data)}\nExpected:\n{behaviour_attr_getter(actual_next_synchronized_data)}" - assert event == exit_event - yield - - -class BaseVotingRoundTest(BaseRoundTestClass): # pylint: disable=too-few-public-methods - """Tests for rounds derived from VotingRound.""" - - def _test_round( # pylint: disable=too-many-arguments,too-many-locals - self, - test_round: VotingRound, - round_payloads: Mapping[str, BaseTxPayload], - synchronized_data_update_fn: Callable, - synchronized_data_attr_checks: List[Callable], - exit_event: Any, - threshold_check: Callable, - ) -> Generator: - """Test for rounds derived from VotingRound.""" - - (_, first_payload), *payloads = round_payloads.items() - - test_round.process_payload(first_payload) - yield test_round - assert not threshold_check(test_round) # negative_vote_threshold_reached - assert test_round.end_block() is None - self._test_no_majority_event(test_round) - - for _, payload in payloads: - test_round.process_payload(payload) - yield test_round - assert threshold_check(test_round) - - actual_next_synchronized_data = synchronized_data_update_fn( - deepcopy(self.synchronized_data), test_round - ) - res = test_round.end_block() - yield res - assert res is not None - - synchronized_data, event = res - - for behaviour_attr_getter in synchronized_data_attr_checks: - assert behaviour_attr_getter(synchronized_data) == behaviour_attr_getter( - actual_next_synchronized_data - ), f"Mismatch in synchronized_data. Actual:\n{behaviour_attr_getter(synchronized_data)}\nExpected:\n{behaviour_attr_getter(actual_next_synchronized_data)}" - assert event == exit_event - yield - - def _test_voting_round_positive( - self, - test_round: VotingRound, - round_payloads: Mapping[str, BaseTxPayload], - synchronized_data_update_fn: Callable, - synchronized_data_attr_checks: List[Callable], - exit_event: Any, - ) -> Generator: - """Test for rounds derived from VotingRound.""" - - return self._test_round( - test_round, - round_payloads, - synchronized_data_update_fn, - synchronized_data_attr_checks, - exit_event, - threshold_check=lambda x: x.positive_vote_threshold_reached, - ) - - def _test_voting_round_negative( - self, - test_round: VotingRound, - round_payloads: Mapping[str, BaseTxPayload], - synchronized_data_update_fn: Callable, - synchronized_data_attr_checks: List[Callable], - exit_event: Any, - ) -> Generator: - """Test for rounds derived from VotingRound.""" - - return self._test_round( - test_round, - round_payloads, - synchronized_data_update_fn, - synchronized_data_attr_checks, - exit_event, - threshold_check=lambda x: x.negative_vote_threshold_reached, - ) - - def _test_voting_round_none( - self, - test_round: VotingRound, - round_payloads: Mapping[str, BaseTxPayload], - synchronized_data_update_fn: Callable, - synchronized_data_attr_checks: List[Callable], - exit_event: Any, - ) -> Generator: - """Test for rounds derived from VotingRound.""" - - return self._test_round( - test_round, - round_payloads, - synchronized_data_update_fn, - synchronized_data_attr_checks, - exit_event, - threshold_check=lambda x: x.none_vote_threshold_reached, - ) - - -class BaseCollectDifferentUntilThresholdRoundTest( # pylint: disable=too-few-public-methods - BaseRoundTestClass -): - """Tests for rounds derived from CollectDifferentUntilThresholdRound.""" - - def _test_round( - self, - test_round: CollectDifferentUntilThresholdRound, - round_payloads: Mapping[str, BaseTxPayload], - synchronized_data_update_fn: Callable, - synchronized_data_attr_checks: List[Callable], - exit_event: Any, - ) -> Generator: - """Test for rounds derived from CollectDifferentUntilThresholdRound.""" - - (_, first_payload), *payloads = round_payloads.items() - - test_round.process_payload(first_payload) - yield test_round - assert not test_round.collection_threshold_reached - assert test_round.end_block() is None - - for _, payload in payloads: - test_round.process_payload(payload) - yield test_round - assert test_round.collection_threshold_reached - - actual_next_synchronized_data = synchronized_data_update_fn( - deepcopy(self.synchronized_data), test_round - ) - - res = test_round.end_block() - yield res - assert res is not None - - synchronized_data, event = res - - for behaviour_attr_getter in synchronized_data_attr_checks: - assert behaviour_attr_getter(synchronized_data) == behaviour_attr_getter( - actual_next_synchronized_data - ), f"Mismatch in synchronized_data. Actual:\n{behaviour_attr_getter(synchronized_data)}\nExpected:\n{behaviour_attr_getter(actual_next_synchronized_data)}" - assert event == exit_event - yield - - -class BaseCollectNonEmptyUntilThresholdRound( # pylint: disable=too-few-public-methods - BaseCollectDifferentUntilThresholdRoundTest -): - """Tests for rounds derived from `CollectNonEmptyUntilThresholdRound`.""" - - -class _BaseRoundTestClass(BaseRoundTestClass): # pylint: disable=too-few-public-methods - """Base test class.""" - - synchronized_data: BaseSynchronizedData - participants: FrozenSet[str] - tx_payloads: List[DummyTxPayload] - - _synchronized_data_class = DummySynchronizedData - - def setup( - self, - ) -> None: - """Setup test class.""" - - super().setup() - self.tx_payloads = get_dummy_tx_payloads(self.participants) - - @staticmethod - def _test_payload_with_wrong_round_count( - test_round: AbstractRound, - value: Optional[str] = None, - vote: Optional[bool] = None, - ) -> None: - """Test errors raised by payloads with wrong round count.""" - payload_with_wrong_round_count = DummyTxPayload("sender", value, vote) - object.__setattr__(payload_with_wrong_round_count, "round_count", 0) - with pytest.raises( - TransactionNotValidError, - match=re.escape("Expected round count -1 and got 0."), - ): - test_round.check_payload(payload=payload_with_wrong_round_count) - - with pytest.raises( - ABCIAppInternalError, - match=re.escape("Expected round count -1 and got 0."), - ): - test_round.process_payload(payload=payload_with_wrong_round_count) diff --git a/packages/valory/skills/abstract_round_abci/tests/__init__.py b/packages/valory/skills/abstract_round_abci/tests/__init__.py deleted file mode 100644 index 7561108..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Tests for valory/abstract_round_abci skill.""" - -from hypothesis import settings # pragma: nocover - - -CI = "CI" # pragma: nocover - -settings.register_profile(CI, deadline=5000) # pragma: nocover diff --git a/packages/valory/skills/abstract_round_abci/tests/conftest.py b/packages/valory/skills/abstract_round_abci/tests/conftest.py deleted file mode 100644 index 5dde4f1..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/conftest.py +++ /dev/null @@ -1,116 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Conftest module for io tests.""" - -import os -import shutil -from contextlib import suppress -from pathlib import Path -from typing import Dict, Generator - -import pytest -from hypothesis import settings - -from packages.valory.skills.abstract_round_abci.io_.store import StoredJSONType -from packages.valory.skills.abstract_round_abci.models import MIN_RESET_PAUSE_DURATION - - -# pylint: skip-file - - -CI = "CI" -PACKAGE_DIR = Path(__file__).parent.parent -settings.register_profile(CI, deadline=5000) -profile_name = ("default", "CI")[bool(os.getenv("CI"))] - - -@pytest.fixture -def dummy_obj() -> StoredJSONType: - """A dummy custom object to test the storing with.""" - return {"test_col": ["test_val_1", "test_val_2"]} - - -@pytest.fixture -def dummy_multiple_obj(dummy_obj: StoredJSONType) -> Dict[str, StoredJSONType]: - """Many dummy custom objects to test the storing with.""" - return {f"test_obj_{i}": dummy_obj for i in range(10)} - - -@pytest.fixture(scope="session", autouse=True) -def hypothesis_cleanup() -> Generator: - """Fixture to remove hypothesis directory after tests.""" - yield - hypothesis_dir = PACKAGE_DIR / ".hypothesis" - if hypothesis_dir.exists(): - with suppress(OSError, PermissionError): # pragma: nocover - shutil.rmtree(hypothesis_dir) - - -# We do not care about these keys but need to set them in the behaviours' tests, -# because `packages.valory.skills.abstract_round_abci.models._ensure` is used. -irrelevant_genesis_config = { - "consensus_params": { - "block": {"max_bytes": "str", "max_gas": "str", "time_iota_ms": "str"}, - "evidence": { - "max_age_num_blocks": "str", - "max_age_duration": "str", - "max_bytes": "str", - }, - "validator": {"pub_key_types": ["str"]}, - "version": {}, - }, - "genesis_time": "str", - "chain_id": "str", - "voting_power": "str", -} -irrelevant_config = { - "tendermint_url": "str", - "max_healthcheck": 0, - "round_timeout_seconds": 0.0, - "sleep_time": 0, - "retry_timeout": 0, - "retry_attempts": 0, - "keeper_timeout": 0.0, - "reset_pause_duration": MIN_RESET_PAUSE_DURATION, - "drand_public_key": "str", - "tendermint_com_url": "str", - "tendermint_max_retries": 0, - "reset_tendermint_after": 0, - "cleanup_history_depth": 0, - "voting_power": 0, - "tendermint_check_sleep_delay": 0, - "cleanup_history_depth_current": None, - "request_timeout": 0.0, - "request_retry_delay": 0.0, - "tx_timeout": 0.0, - "max_attempts": 0, - "service_registry_address": None, - "on_chain_service_id": None, - "share_tm_config_on_startup": False, - "tendermint_p2p_url": "str", - "setup": {}, - "genesis_config": irrelevant_genesis_config, - "use_termination": False, - "use_slashing": False, - "slash_cooldown_hours": 3, - "slash_threshold_amount": 10_000_000_000_000_000, - "light_slash_unit_amount": 5_000_000_000_000_000, - "serious_slash_unit_amount": 8_000_000_000_000_000, -} diff --git a/packages/valory/skills/abstract_round_abci/tests/data/__init__.py b/packages/valory/skills/abstract_round_abci/tests/data/__init__.py deleted file mode 100644 index 7266f78..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/data/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Tests for abstract_round_abci/test_tools""" diff --git a/packages/valory/skills/abstract_round_abci/tests/test_abci_app_chain.py b/packages/valory/skills/abstract_round_abci/tests/test_abci_app_chain.py deleted file mode 100644 index 4a95580..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/test_abci_app_chain.py +++ /dev/null @@ -1,508 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test the abci_app_chain.py module of the skill.""" - -# pylint: skip-file - -import logging -from typing import Dict, Set, Tuple, Type -from unittest.mock import MagicMock - -import pytest -from _pytest.logging import LogCaptureFixture -from aea.exceptions import AEAEnforceError - -from packages.valory.skills.abstract_round_abci.abci_app_chain import ( - AbciAppTransitionMapping, - chain, -) -from packages.valory.skills.abstract_round_abci.base import ( - AbciApp, - AbciAppDB, - AbstractRound, - AppState, - BaseSynchronizedData, - BaseTxPayload, - DegenerateRound, -) - - -def make_round_class(name: str, bases: Tuple = (AbstractRound,)) -> Type[AbstractRound]: - """Make a round class.""" - new_round_cls = type( - name, - bases, - { - "synchronized_data_class": MagicMock(), - "payload_class": MagicMock(), - "payload_attribute": MagicMock(), - }, - ) - setattr(new_round_cls, "round_id", name) # noqa: B010 - assert issubclass(new_round_cls, AbstractRound) # nosec - return new_round_cls - - -class TestAbciAppChaining: - """Test chaning of AbciApps.""" - - def setup(self) -> None: - """Setup test.""" - self.round_1a = make_round_class("round_1a") - self.round_1b = make_round_class("round_1b") - self.round_1b_dupe = make_round_class("round_1b") # duplicated round id - self.round_1c = make_round_class("round_1c", (DegenerateRound,)) - - self.round_2a = make_round_class("round_2a") - self.round_2b = make_round_class("round_2b") - self.round_2c = make_round_class("round_2c", (DegenerateRound,)) - self.round_2d = make_round_class("round_2d") - - self.round_3a = make_round_class("round_3a") - self.round_3b = make_round_class("round_3b") - self.round_3c = make_round_class("round_3c", (DegenerateRound,)) - - self.key_1 = "1" - self.key_2 = "2" - self.key_3 = "3" - - self.event_1a = "event_1a" - self.event_1b = "event_1b" - self.event_1c = "event_1c" - self.event_timeout1 = "timeout_1" - - self.event_2a = "event_2a" - self.event_2b = "event_2b" - self.event_2c = "event_2c" - self.event_timeout2 = "timeout_2" - - self.event_3a = "event_3a" - self.event_3b = "event_3b" - self.event_3c = "event_3c" - self.event_timeout3 = "timeout_3" - - self.timeout1 = 10.0 - self.timeout2 = 15.0 - self.timeout3 = 20.0 - - self.cross_period_persisted_keys_1 = frozenset({"1", "2"}) - self.cross_period_persisted_keys_2 = frozenset({"2", "3"}) - - class AbciApp1(AbciApp): - initial_round_cls = self.round_1a - transition_function = { - self.round_1a: { - self.event_timeout1: self.round_1a, - self.event_1b: self.round_1b, - }, - self.round_1b: { - self.event_1a: self.round_1a, - self.event_1c: self.round_1c, - }, - self.round_1c: {}, - } - final_states = {self.round_1c} - event_to_timeout = {self.event_timeout1: self.timeout1} - db_pre_conditions: Dict[AppState, Set[str]] = {self.round_1a: set()} - db_post_conditions: Dict[AppState, Set[str]] = {self.round_1c: {self.key_1}} - cross_period_persisted_keys = self.cross_period_persisted_keys_1 - - self.app1_class = AbciApp1 - - class AbciApp2(AbciApp): - initial_round_cls = self.round_2a - transition_function = { - self.round_2a: { - self.event_timeout2: self.round_2a, - self.event_2b: self.round_2b, - }, - self.round_2b: { - self.event_2a: self.round_2a, - self.event_2c: self.round_2c, - }, - self.round_2c: {}, - } - final_states = {self.round_2c} - event_to_timeout = {self.event_timeout2: self.timeout2} - db_pre_conditions: Dict[AppState, Set[str]] = {self.round_2a: {self.key_1}} - db_post_conditions: Dict[AppState, Set[str]] = {self.round_2c: {self.key_2}} - cross_period_persisted_keys = self.cross_period_persisted_keys_2 - - self.app2_class = AbciApp2 - - class AbciApp3(AbciApp): - initial_round_cls = self.round_3a - transition_function = { - self.round_3a: { - self.event_timeout3: self.round_3a, - self.event_3b: self.round_3b, - }, - self.round_3b: { - self.event_3a: self.round_3a, - self.event_3c: self.round_3c, - self.event_1a: self.round_3a, # duplicated event - }, - self.round_3c: {}, - } - final_states = {self.round_3c} - event_to_timeout = {self.event_timeout3: self.timeout3} - db_pre_conditions: Dict[AppState, Set[str]] = { - self.round_3a: {self.key_1, self.key_2} - } - db_post_conditions: Dict[AppState, Set[str]] = {self.round_3c: {self.key_3}} - - self.app3_class = AbciApp3 - - class AbciApp3Dupe(AbciApp): - initial_round_cls = self.round_3a - transition_function = { - self.round_3a: { - self.event_timeout3: self.round_3a, - self.event_3b: self.round_3b, - }, - self.round_1b_dupe: { # duplucated round id - self.event_3a: self.round_3a, - self.event_3c: self.round_3c, - self.event_1a: self.round_3a, # duplicated event - }, - self.round_3c: {}, - } - final_states = {self.round_3c} - event_to_timeout = {self.event_timeout3: self.timeout3} - db_post_conditions: Dict[AppState, Set[str]] = {self.round_3c: set()} - - self.app3_class_dupe = AbciApp3Dupe - - class AbciApp2Faulty1(AbciApp): - initial_round_cls = self.round_2a - transition_function = { - self.round_2a: { - self.event_timeout2: self.round_2a, - self.event_2b: self.round_2b, - }, - self.round_2b: { - self.event_2a: self.round_2a, - self.event_2c: self.round_2c, - }, - self.round_2c: {}, - } - final_states = {self.round_2c} - event_to_timeout = {self.event_timeout1: self.timeout2} - db_pre_conditions: Dict[AppState, Set[str]] = {self.round_2a: {self.key_1}} - db_post_conditions: Dict[AppState, Set[str]] = {self.round_2c: {self.key_2}} - - self.app2_class_faulty1 = AbciApp2Faulty1 - - def test_chain_two(self) -> None: - """Test the AbciApp chain function.""" - - abci_app_transition_mapping: AbciAppTransitionMapping = { - self.round_1c: self.round_2a, - self.round_2c: self.round_1a, - } - - ComposedAbciApp = chain( - (self.app1_class, self.app2_class), abci_app_transition_mapping - ) - - assert ComposedAbciApp.initial_round_cls == self.round_1a - assert ComposedAbciApp.transition_function == { - self.round_1a: { - self.event_timeout1: self.round_1a, - self.event_1b: self.round_1b, - }, - self.round_1b: { - self.event_1a: self.round_1a, - self.event_1c: self.round_2a, - }, - self.round_2a: { - self.event_timeout2: self.round_2a, - self.event_2b: self.round_2b, - }, - self.round_2b: { - self.event_2a: self.round_2a, - self.event_2c: self.round_1a, - }, - } - assert ComposedAbciApp.final_states == set() - assert ComposedAbciApp.event_to_timeout == { - self.event_timeout1: self.timeout1, - self.event_timeout2: self.timeout2, - } - assert ( - ComposedAbciApp.cross_period_persisted_keys - == self.cross_period_persisted_keys_1.union( - self.cross_period_persisted_keys_2 - ) - ) - - def test_chain_three(self) -> None: - """Test the AbciApp chain function.""" - - abci_app_transition_mapping: AbciAppTransitionMapping = { - self.round_1c: self.round_2a, - self.round_2c: self.round_3a, - } - - ComposedAbciApp = chain( - (self.app1_class, self.app2_class, self.app3_class), - abci_app_transition_mapping, - ) - - assert ComposedAbciApp.initial_round_cls == self.round_1a - assert ComposedAbciApp.transition_function == { - self.round_1a: { - self.event_timeout1: self.round_1a, - self.event_1b: self.round_1b, - }, - self.round_1b: { - self.event_1a: self.round_1a, - self.event_1c: self.round_2a, - }, - self.round_2a: { - self.event_timeout2: self.round_2a, - self.event_2b: self.round_2b, - }, - self.round_2b: { - self.event_2a: self.round_2a, - self.event_2c: self.round_3a, - }, - self.round_3a: { - self.event_timeout3: self.round_3a, - self.event_3b: self.round_3b, - }, - self.round_3b: { - self.event_3a: self.round_3a, - self.event_3c: self.round_3c, - self.event_1a: self.round_3a, - }, - self.round_3c: {}, - } - assert ComposedAbciApp.final_states == {self.round_3c} - assert ComposedAbciApp.event_to_timeout == { - self.event_timeout1: self.timeout1, - self.event_timeout2: self.timeout2, - self.event_timeout3: self.timeout3, - } - - def test_chain_two_negative_timeouts(self) -> None: - """Test the AbciApp chain function.""" - - abci_app_transition_mapping: AbciAppTransitionMapping = { - self.round_1c: self.round_2a, - self.round_2c: self.round_1a, - } - - with pytest.raises( - ValueError, match="but it is already defined in a prior app with timeout" - ): - _ = chain( - (self.app1_class, self.app2_class_faulty1), abci_app_transition_mapping - ) - - def test_chain_two_negative_mapping_initial_states(self) -> None: - """Test the AbciApp chain function.""" - - abci_app_transition_mapping: AbciAppTransitionMapping = { - self.round_1c: self.round_2b, - self.round_2c: self.round_1a, - } - - with pytest.raises(ValueError, match="Found non-initial state"): - _ = chain((self.app1_class, self.app2_class), abci_app_transition_mapping) - - def test_chain_two_negative_mapping_final_states(self) -> None: - """Test the AbciApp chain function.""" - - abci_app_transition_mapping: AbciAppTransitionMapping = { - self.round_1c: self.round_2a, - self.round_2b: self.round_1a, - } - - with pytest.raises(ValueError, match="Found non-final state"): - _ = chain((self.app1_class, self.app2_class), abci_app_transition_mapping) - - def test_chain_two_dupe(self) -> None: - """Test the AbciApp chain function.""" - - abci_app_transition_mapping: AbciAppTransitionMapping = { - self.round_1c: self.round_2a, - self.round_2c: self.round_1a, - } - with pytest.raises( - AEAEnforceError, - match=r"round ids in common between abci apps are not allowed.*", - ): - chain((self.app1_class, self.app3_class_dupe), abci_app_transition_mapping) - - def test_chain_with_abstract_abci_app_fails(self) -> None: - """Test chaining with an abstract AbciApp fails.""" - self.app2_class._is_abstract = False - self.app3_class._is_abstract = False - with pytest.raises( - AEAEnforceError, - match=r"found non-abstract AbciApp during chaining: \['AbciApp2', 'AbciApp3'\]", - ): - abci_app_transition_mapping: AbciAppTransitionMapping = { - self.round_1c: self.round_2a, - self.round_2c: self.round_3a, - } - chain( - (self.app1_class, self.app2_class, self.app3_class), - abci_app_transition_mapping, - ) - - def test_synchronized_data_type(self, caplog: LogCaptureFixture) -> None: - """Test synchronized data type""" - - abci_app_transition_mapping: AbciAppTransitionMapping = { - self.round_1c: self.round_2a, - self.round_2c: self.round_1a, - } - - sentinel_app1 = object() - sentinel_app2 = object() - - def make_sync_data(sentinel: object) -> Type: - class SynchronizedData(BaseSynchronizedData): - @property - def dummy_attr(self) -> object: - return sentinel - - return SynchronizedData - - def make_concrete(round_cls: Type[AbstractRound]) -> Type[AbstractRound]: - class ConcreteRound(round_cls): # type: ignore - def check_payload(self, payload: BaseTxPayload) -> None: - pass - - def process_payload(self, payload: BaseTxPayload) -> None: - pass - - def end_block(self) -> None: - pass - - payload_class = None - - return ConcreteRound - - sync_data_cls_app1 = make_sync_data(sentinel_app1) - sync_data_cls_app2 = make_sync_data(sentinel_app2) - - # don't need to mock all of this since setup creates these classes - for abci_app_cls, sync_data_cls in ( - (self.app1_class, sync_data_cls_app1), - (self.app2_class, sync_data_cls_app2), - ): - synchronized_data = sync_data_cls(db=AbciAppDB(setup_data={})) - abci_app = abci_app_cls(synchronized_data, logging.getLogger(), MagicMock()) - for r in abci_app_cls.get_all_rounds(): - r.synchronized_data_class = sync_data_cls - - abci_app_cls = chain( - (self.app1_class, self.app2_class), abci_app_transition_mapping - ) - synchronized_data = sync_data_cls_app2(db=AbciAppDB(setup_data={})) - abci_app = abci_app_cls(synchronized_data, logging.getLogger(), MagicMock()) - - assert abci_app.initial_round_cls == self.round_1a - assert isinstance(abci_app.synchronized_data, sync_data_cls_app1) - assert abci_app.synchronized_data.dummy_attr == sentinel_app1 - - app2_classes = self.app2_class.get_all_rounds() - for round_ in sorted(abci_app.get_all_rounds(), key=str): - abci_app._round_results.append(abci_app.synchronized_data) - abci_app.schedule_round(make_concrete(round_)) - expected_cls = (sync_data_cls_app1, sync_data_cls_app2)[ - round_ in app2_classes - ] - assert isinstance(abci_app.synchronized_data, expected_cls) - expected_sentinel = (sentinel_app1, sentinel_app2)[round_ in app2_classes] - assert abci_app.synchronized_data.dummy_attr == expected_sentinel - - def test_precondition_for_next_app_missing_raises( - self, caplog: LogCaptureFixture - ) -> None: - """Test that when precondition for next AbciApp is missing an error is raised""" - - class AbciApp1(AbciApp): - initial_round_cls = self.round_1a - transition_function = { - self.round_1a: { - self.event_timeout1: self.round_1a, - self.event_1b: self.round_1b, - }, - self.round_1b: { - self.event_1a: self.round_1a, - self.event_1c: self.round_1c, - }, - self.round_1c: {}, - } - final_states = {self.round_1c} - event_to_timeout = {self.event_timeout1: self.timeout1} - db_pre_conditions: Dict[AppState, Set[str]] = {self.round_1a: set()} - db_post_conditions: Dict[AppState, Set[str]] = {self.round_1c: set()} - cross_period_persisted_keys = self.cross_period_persisted_keys_1 - - abci_app_transition_mapping: AbciAppTransitionMapping = { - self.round_1c: self.round_2a, - self.round_2c: self.round_1a, - } - - expected = "Pre conditions '.*' of app '.*' not a post condition of app '.*' or any preceding app in path .*." - with pytest.raises(ValueError, match=expected): - chain( - ( - AbciApp1, - self.app2_class, - ), - abci_app_transition_mapping, - ) - - def test_precondition_app_missing_raises(self, caplog: LogCaptureFixture) -> None: - """Test that missing precondition specification for next AbciApp is missing an error is raised""" - - class AbciApp2(AbciApp): - initial_round_cls = self.round_2a - transition_function = { - self.round_2a: { - self.event_timeout2: self.round_2a, - self.event_2b: self.round_2b, - }, - self.round_2b: { - self.event_2a: self.round_2a, - self.event_2c: self.round_2c, - }, - self.round_2c: {}, - } - final_states = {self.round_2c} - event_to_timeout = {self.event_timeout2: self.timeout2} - db_pre_conditions: Dict[AppState, Set[str]] = {} - db_post_conditions: Dict[AppState, Set[str]] = {self.round_2c: {self.key_2}} - cross_period_persisted_keys = self.cross_period_persisted_keys_2 - - abci_app_transition_mapping: AbciAppTransitionMapping = { - self.round_1c: self.round_2a, - self.round_2c: self.round_1a, - } - - expected = "No pre-conditions have been set for .*! You need to explicitly specify them as empty if there are no pre-conditions for this FSM." - with pytest.raises(ValueError, match=expected): - chain((self.app1_class, AbciApp2), abci_app_transition_mapping) diff --git a/packages/valory/skills/abstract_round_abci/tests/test_base.py b/packages/valory/skills/abstract_round_abci/tests/test_base.py deleted file mode 100644 index c5ff0f4..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/test_base.py +++ /dev/null @@ -1,3420 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test the base.py module of the skill.""" - -import dataclasses -import datetime -import json -import logging -import re -import shutil -from abc import ABC -from calendar import timegm -from collections import deque -from contextlib import suppress -from copy import copy, deepcopy -from dataclasses import dataclass -from enum import Enum -from pathlib import Path -from time import sleep -from typing import ( - Any, - Callable, - Deque, - Dict, - FrozenSet, - Generator, - List, - Optional, - Set, - Tuple, - Type, - cast, -) -from unittest import mock -from unittest.mock import MagicMock - -import pytest -from _pytest.logging import LogCaptureFixture -from aea.exceptions import AEAEnforceError -from aea_ledger_ethereum import EthereumCrypto -from hypothesis import HealthCheck, given, settings -from hypothesis.strategies import ( - DrawFn, - binary, - booleans, - builds, - composite, - data, - datetimes, - dictionaries, - floats, - integers, - just, - lists, - none, - one_of, - sampled_from, - text, -) - -import packages.valory.skills.abstract_round_abci.base as abci_base -from packages.valory.connections.abci.connection import MAX_READ_IN_BYTES -from packages.valory.protocols.abci.custom_types import ( - Evidence, - EvidenceType, - Evidences, - LastCommitInfo, - Timestamp, - Validator, - VoteInfo, -) -from packages.valory.skills.abstract_round_abci.base import ( - ABCIAppException, - ABCIAppInternalError, - AbciApp, - AbciAppDB, - AbciAppTransitionFunction, - AbstractRound, - AbstractRoundInternalError, - AddBlockError, - AppState, - AvailabilityWindow, - BaseSynchronizedData, - BaseTxPayload, - Block, - BlockBuilder, - Blockchain, - CollectionRound, - EventType, - LateArrivingTransaction, - OffenceStatus, - OffenseStatusDecoder, - OffenseStatusEncoder, - OffenseType, - RoundSequence, - SignatureNotValidError, - SlashingNotConfiguredError, - Timeouts, - Transaction, - TransactionTypeNotRecognizedError, - _MetaAbciApp, - _MetaAbstractRound, - _MetaPayload, - get_name, - light_offences, - serious_offences, -) -from packages.valory.skills.abstract_round_abci.test_tools.abci_app import ( - AbciAppTest, - ConcreteBackgroundRound, - ConcreteBackgroundSlashingRound, - ConcreteEvents, - ConcreteRoundA, - ConcreteRoundB, - ConcreteRoundC, - ConcreteSlashingRoundA, - ConcreteTerminationRoundA, - ConcreteTerminationRoundB, - ConcreteTerminationRoundC, - SlashingAppTest, - TerminationAppTest, -) -from packages.valory.skills.abstract_round_abci.test_tools.rounds import ( - BaseRoundTestClass, - get_participants, -) -from packages.valory.skills.abstract_round_abci.tests.conftest import profile_name - - -# pylint: skip-file - - -settings.load_profile(profile_name) - - -PACKAGE_DIR = Path(__file__).parent.parent - - -DUMMY_CONCRETE_BACKGROUND_PAYLOAD = ConcreteBackgroundRound.payload_class( - sender="sender" -) - - -@pytest.fixture(scope="session", autouse=True) -def hypothesis_cleanup() -> Generator: - """Fixture to remove hypothesis directory after tests.""" - yield - hypothesis_dir = PACKAGE_DIR / ".hypothesis" - if hypothesis_dir.exists(): - with suppress(OSError, PermissionError): - shutil.rmtree(hypothesis_dir) - - -class BasePayload(BaseTxPayload, ABC): - """Base payload class for testing.""" - - -@dataclass(frozen=True) -class PayloadA(BasePayload): - """Payload class for payload type 'A'.""" - - -@dataclass(frozen=True) -class PayloadB(BasePayload): - """Payload class for payload type 'B'.""" - - -@dataclass(frozen=True) -class PayloadC(BasePayload): - """Payload class for payload type 'C'.""" - - -@dataclass(frozen=True) -class PayloadD(BasePayload): - """Payload class for payload type 'D'.""" - - -@dataclass(frozen=True) -class DummyPayload(BasePayload): - """Dummy payload class.""" - - dummy_attribute: int - - -@dataclass(frozen=True) -class TooBigPayload(BaseTxPayload): - """Base payload class for testing.""" - - dummy_field: str = "0" * 10**7 - - -class ObjectImitator: - """For custom __eq__ implementation testing""" - - def __init__(self, other: Any): - """Copying references to class attr, and instance attr""" - - for attr, value in vars(other.__class__).items(): - if not attr.startswith("__") and not attr.endswith("__"): - setattr(self.__class__, attr, value) - - self.__dict__ = other.__dict__ - - -def test_base_tx_payload() -> None: - """Test BaseTxPayload.""" - - payload = PayloadA(sender="sender") - object.__setattr__(payload, "round_count", 9) - new_payload = payload.with_new_id() - - assert not payload == new_payload - payload_data, new_payload_data = payload.json, new_payload.json - assert not payload_data.pop("id_") == new_payload_data.pop("id_") - assert payload_data == new_payload_data - with pytest.raises(dataclasses.FrozenInstanceError): - payload.round_count = 1 # type: ignore - object.__setattr__(payload, "round_count", 1) - assert payload.round_count == 1 - assert type(hash(payload)) == int - - -def test_meta_round_abstract_round_when_instance_not_subclass_of_abstract_round() -> ( - None -): - """Test instantiation of meta class when instance not a subclass of abstract round.""" - - class MyAbstractRound(metaclass=_MetaAbstractRound): - pass - - -def test_abstract_round_instantiation_without_attributes_raises_error() -> None: - """Test that definition of concrete subclass of AbstractRound without attributes raises error.""" - with pytest.raises(AbstractRoundInternalError): - - class MyRoundBehaviour(AbstractRound): - pass - - with pytest.raises(AbstractRoundInternalError): - - class MyRoundBehaviourB(AbstractRound): - synchronized_data_class = MagicMock() - - -class TestTransactions: - """Test Transactions class.""" - - def setup(self) -> None: - """Set up the test.""" - self.old_value = copy(_MetaPayload.registry) - - def test_encode_decode(self) -> None: - """Test encoding and decoding of payloads.""" - sender = "sender" - - expected_payload = PayloadA(sender=sender) - actual_payload = PayloadA.decode(expected_payload.encode()) - assert expected_payload == actual_payload - - expected_payload_ = PayloadB(sender=sender) - actual_payload_ = PayloadB.decode(expected_payload_.encode()) - assert expected_payload_ == actual_payload_ - - expected_payload__ = PayloadC(sender=sender) - actual_payload__ = PayloadC.decode(expected_payload__.encode()) - assert expected_payload__ == actual_payload__ - - expected_payload___ = PayloadD(sender=sender) - actual_payload___ = PayloadD.decode(expected_payload___.encode()) - assert expected_payload___ == actual_payload___ - - def test_encode_decode_transaction(self) -> None: - """Test encode/decode of a transaction.""" - sender = "sender" - signature = "signature" - payload = PayloadA(sender) - expected = Transaction(payload, signature) - actual = expected.decode(expected.encode()) - assert expected == actual - - def test_encode_too_big_payload(self) -> None: - """Test encode of a too big payload.""" - sender = "sender" - payload = TooBigPayload(sender) - with pytest.raises( - ValueError, - match=f"{type(payload)} must be smaller than {MAX_READ_IN_BYTES} bytes", - ): - payload.encode() - - def test_encode_too_big_transaction(self) -> None: - """Test encode of a too big transaction.""" - sender = "sender" - signature = "signature" - payload = TooBigPayload(sender) - tx = Transaction(payload, signature) - with pytest.raises( - ValueError, - match=f"Transaction must be smaller than {MAX_READ_IN_BYTES} bytes", - ): - tx.encode() - - def test_sign_verify_transaction(self) -> None: - """Test sign/verify transaction.""" - crypto = EthereumCrypto() - sender = crypto.address - payload = PayloadA(sender) - payload_bytes = payload.encode() - signature = crypto.sign_message(payload_bytes) - transaction = Transaction(payload, signature) - transaction.verify(crypto.identifier) - - def test_payload_not_equal_lookalike(self) -> None: - """Test payload __eq__ reflection via NotImplemented""" - payload = PayloadA(sender="sender") - lookalike = ObjectImitator(payload) - assert not payload == lookalike - - def test_transaction_not_equal_lookalike(self) -> None: - """Test transaction __eq__ reflection via NotImplemented""" - payload = PayloadA(sender="sender") - transaction = Transaction(payload, signature="signature") - lookalike = ObjectImitator(transaction) - assert not transaction == lookalike - - def teardown(self) -> None: - """Tear down the test.""" - _MetaPayload.registry = self.old_value - - -@mock.patch( - "aea.crypto.ledger_apis.LedgerApis.recover_message", return_value={"wrong_sender"} -) -def test_verify_transaction_negative_case(*_mocks: Any) -> None: - """Test verify() of transaction, negative case.""" - transaction = Transaction(MagicMock(sender="right_sender", json={}), "") - with pytest.raises( - SignatureNotValidError, match="Signature not valid on transaction: .*" - ): - transaction.verify("") - - -@dataclass(frozen=True) -class SomeClass(BaseTxPayload): - """Test class.""" - - content: Dict - - -@given( - dictionaries( - keys=text(), - values=one_of(floats(allow_nan=False, allow_infinity=False), booleans()), - ) -) -def test_payload_serializer_is_deterministic(obj: Any) -> None: - """Test that 'DictProtobufStructSerializer' is deterministic.""" - obj_ = SomeClass(sender="", content=obj) - obj_bytes = obj_.encode() - assert obj_ == BaseTxPayload.decode(obj_bytes) - - -def test_initialize_block() -> None: - """Test instantiation of a Block instance.""" - block = Block(MagicMock(), []) - assert block.transactions == tuple() - - -class TestBlockchain: - """Test a blockchain object.""" - - def setup(self) -> None: - """Set up the test.""" - self.blockchain = Blockchain() - - def test_height(self) -> None: - """Test the 'height' property getter.""" - assert self.blockchain.height == 0 - - def test_len(self) -> None: - """Test the 'length' property getter.""" - assert self.blockchain.length == 0 - - def test_add_block_positive(self) -> None: - """Test 'add_block', success.""" - block = Block(MagicMock(height=1), []) - self.blockchain.add_block(block) - assert self.blockchain.length == 1 - assert self.blockchain.height == 1 - - def test_add_block_negative_wrong_height(self) -> None: - """Test 'add_block', wrong height.""" - wrong_height = 42 - block = Block(MagicMock(height=wrong_height), []) - with pytest.raises( - AddBlockError, - match=f"expected height {self.blockchain.height + 1}, got {wrong_height}", - ): - self.blockchain.add_block(block) - - def test_add_block_before_initial_height(self) -> None: - """Test 'add_block', too old height.""" - height_offset = 42 - blockchain = Blockchain(height_offset=height_offset) - block = Block(MagicMock(height=height_offset - 1), []) - blockchain.add_block(block) - - def test_blocks(self) -> None: - """Test 'blocks' property getter.""" - assert self.blockchain.blocks == tuple() - - -class TestBlockBuilder: - """Test block builder.""" - - def setup(self) -> None: - """Set up the method.""" - self.block_builder = BlockBuilder() - - def test_get_header_positive(self) -> None: - """Test header property getter, positive.""" - expected_header = MagicMock() - self.block_builder._current_header = expected_header - actual_header = self.block_builder.header - assert expected_header == actual_header - - def test_get_header_negative(self) -> None: - """Test header property getter, negative.""" - with pytest.raises(ValueError, match="header not set"): - self.block_builder.header - - def test_set_header_positive(self) -> None: - """Test header property setter, positive.""" - expected_header = MagicMock() - self.block_builder.header = expected_header - actual_header = self.block_builder.header - assert expected_header == actual_header - - def test_set_header_negative(self) -> None: - """Test header property getter, negative.""" - self.block_builder.header = MagicMock() - with pytest.raises(ValueError, match="header already set"): - self.block_builder.header = MagicMock() - - def test_transitions_getter(self) -> None: - """Test 'transitions' property getter.""" - assert self.block_builder.transactions == tuple() - - def test_add_transitions(self) -> None: - """Test 'add_transition'.""" - transaction = MagicMock() - self.block_builder.add_transaction(transaction) - assert self.block_builder.transactions == (transaction,) - - def test_get_block_negative_header_not_set_yet(self) -> None: - """Test 'get_block', negative case (header not set yet).""" - with pytest.raises(ValueError, match="header not set"): - self.block_builder.get_block() - - def test_get_block_positive(self) -> None: - """Test 'get_block', positive case.""" - self.block_builder.header = MagicMock() - self.block_builder.get_block() - - -class TestAbciAppDB: - """Test 'AbciAppDB' class.""" - - def setup(self) -> None: - """Set up the tests.""" - self.participants = ("a", "b") - self.db = AbciAppDB( - setup_data=dict(participants=[self.participants]), - ) - - @pytest.mark.parametrize( - "data, setup_data", - ( - ({"participants": ["a", "b"]}, {"participants": ["a", "b"]}), - ({"participants": []}, {}), - ({"participants": None}, None), - ("participants", None), - (1, None), - (object(), None), - (["participants"], None), - ({"participants": [], "other": [1, 2]}, {"other": [1, 2]}), - ), - ) - @pytest.mark.parametrize( - "cross_period_persisted_keys, expected_cross_period_persisted_keys", - ((None, set()), (set(), set()), ({"test"}, {"test"})), - ) - def test_init( - self, - data: Dict, - setup_data: Optional[Dict], - cross_period_persisted_keys: Optional[Set[str]], - expected_cross_period_persisted_keys: Set[str], - ) -> None: - """Test constructor.""" - # keys are a set, but we cast them to a frozenset, so we can still update them and also make `mypy` - # think that the type is correct, to simulate a user incorrectly passing a different type and check if the - # attribute can be altered - cast_keys = cast(Optional[FrozenSet[str]], cross_period_persisted_keys) - # update with the default keys - expected_cross_period_persisted_keys.update(AbciAppDB.default_cross_period_keys) - - if setup_data is None: - # the parametrization of `setup_data` set to `None` is in order to check if the exception is raised - # when we incorrectly set the data in the configuration file with a type that is not allowed - with pytest.raises( - ValueError, - match=re.escape( - f"AbciAppDB data must be `Dict[str, List[Any]]`, found `{type(data)}` instead" - ), - ): - AbciAppDB( - data, - ) - return - - # use copies because otherwise the arguments will be modified and the next test runs will be polluted - data_copy = deepcopy(data) - cross_period_persisted_keys_copy = cast_keys.copy() if cast_keys else cast_keys - db = AbciAppDB(data_copy, cross_period_persisted_keys_copy) - assert db._data == {0: setup_data} - assert db.setup_data == setup_data - assert db.cross_period_persisted_keys == expected_cross_period_persisted_keys - - def data_assertion() -> None: - """Assert that the data are fine.""" - assert db._data == {0: setup_data} and db.setup_data == setup_data, ( - "The database's `setup_data` have been altered indirectly, " - "by updating an item passed via the `__init__`!" - ) - - new_value_attempt = "new_value_attempt" - data_copy.update({"dummy_key": [new_value_attempt]}) - data_assertion() - - data_copy["participants"].append(new_value_attempt) - data_assertion() - - if cross_period_persisted_keys_copy: - # cast back to set - cast(Set[str], cross_period_persisted_keys_copy).add(new_value_attempt) - assert ( - db.cross_period_persisted_keys == expected_cross_period_persisted_keys - ), ( - "The database's `cross_period_persisted_keys` have been altered indirectly, " - "by updating an item passed via the `__init__`!" - ) - - class EnumTest(Enum): - """A test Enum class""" - - test = 10 - - @pytest.mark.parametrize( - "data_in, expected_output", - ( - (0, 0), - ([], []), - ({"test": 2}, {"test": 2}), - (EnumTest.test, 10), - (b"test", b"test".hex()), - ({3, 4}, "[3, 4]"), - (object(), None), - ), - ) - def test_normalize(self, data_in: Any, expected_output: Any) -> None: - """Test `normalize`.""" - if expected_output is None: - with pytest.raises(ValueError): - self.db.normalize(data_in) - return - assert self.db.normalize(data_in) == expected_output - - @pytest.mark.parametrize("data", {0: [{"test": 2}]}) - def test_reset_index(self, data: Dict) -> None: - """Test `reset_index`.""" - assert self.db.reset_index == 0 - self.db.sync(self.db.serialize()) - assert self.db.reset_index == 0 - - def test_round_count_setter(self) -> None: - """Tests the round count setter.""" - expected_value = 1 - - # assume the round count is 0 in the begging - self.db._round_count = 0 - - # update to one via the setter - self.db.round_count = expected_value - - assert self.db.round_count == expected_value - - def test_try_alter_init_data(self) -> None: - """Test trying to alter the init data.""" - data_key = "test" - data_value = [data_key] - expected_data = {data_key: data_value} - passed_data = {data_key: data_value.copy()} - db = AbciAppDB(passed_data) - assert db.setup_data == expected_data - - mutability_error_message = ( - "The database's `setup_data` have been altered indirectly, " - "by updating an item retrieved via the `setup_data` property!" - ) - - db.setup_data.update({data_key: ["altered"]}) - assert db.setup_data == expected_data, mutability_error_message - - db.setup_data[data_key].append("altered") - assert db.setup_data == expected_data, mutability_error_message - - def test_cross_period_persisted_keys(self) -> None: - """Test `cross_period_persisted_keys` property""" - setup_data: Dict[str, List] = {} - cross_period_persisted_keys = frozenset({"test"}) - db = AbciAppDB(setup_data, cross_period_persisted_keys.copy()) - - assert isinstance(db.cross_period_persisted_keys, frozenset), ( - "The database's `cross_period_persisted_keys` can be altered indirectly. " - "The `cross_period_persisted_keys` was expected to be a `frozenset`!" - ) - - def test_get(self) -> None: - """Test getters.""" - assert self.db.get("participants", default="default") == self.participants - assert self.db.get("inexistent", default="default") == "default" - assert self.db.get_latest_from_reset_index(0) == { - "participants": self.participants - } - assert self.db.get_latest() == {"participants": self.participants} - - mutable_key = "mutable" - mutable_value = ["test"] - self.db.update(**{mutable_key: mutable_value.copy()}) - mutable_getters = set() - for getter, kwargs in ( - ("get", {"key": mutable_key}), - ("get_strict", {"key": mutable_key}), - ("get_latest_from_reset_index", {"reset_index": 0}), - ("get_latest", {}), - ): - retrieved = getattr(self.db, getter)(**kwargs) - if getter.startswith("get_latest"): - retrieved = retrieved[mutable_key] - retrieved.append("new_value_attempt") - - if self.db.get(mutable_key) != mutable_value: - mutable_getters.add(getter) - - assert not mutable_getters, ( - "The database has been altered indirectly, " - f"by updating the item(s) retrieved via the `{mutable_getters}` method(s)!" - ) - - def test_increment_round_count(self) -> None: - """Test increment_round_count.""" - assert self.db.round_count == -1 - self.db.increment_round_count() - assert self.db.round_count == 0 - - @mock.patch.object( - abci_base, - "is_json_serializable", - return_value=False, - ) - def test_validate(self, _: mock._patch) -> None: - """Test `validate` method.""" - data = "does not matter" - - with pytest.raises( - ABCIAppInternalError, - match=re.escape( - "internal error: `AbciAppDB` data must be json-serializable. Please convert non-serializable data in " - f"`{data}`. You may use `AbciAppDB.validate(your_data)` to validate your data for the `AbciAppDB`." - ), - ): - AbciAppDB.validate(data) - - @pytest.mark.parametrize( - "setup_data, update_data, expected_data", - ( - (dict(), {"dummy_key": "dummy_value"}, {0: {"dummy_key": ["dummy_value"]}}), - ( - dict(), - {"dummy_key": ["dummy_value1", "dummy_value2"]}, - {0: {"dummy_key": [["dummy_value1", "dummy_value2"]]}}, - ), - ( - {"test": ["test"]}, - {"dummy_key": "dummy_value"}, - {0: {"dummy_key": ["dummy_value"], "test": ["test"]}}, - ), - ( - {"test": ["test"]}, - {"test": "dummy_value"}, - {0: {"test": ["test", "dummy_value"]}}, - ), - ( - {"test": [["test"]]}, - {"test": ["dummy_value1", "dummy_value2"]}, - {0: {"test": [["test"], ["dummy_value1", "dummy_value2"]]}}, - ), - ( - {"test": ["test"]}, - {"test": ["dummy_value1", "dummy_value2"]}, - {0: {"test": ["test", ["dummy_value1", "dummy_value2"]]}}, - ), - ), - ) - def test_update( - self, setup_data: Dict, update_data: Dict, expected_data: Dict[int, Dict] - ) -> None: - """Test update db.""" - db = AbciAppDB(setup_data) - db.update(**update_data) - assert db._data == expected_data - - mutable_key = "mutable" - mutable_value = ["test"] - update_data = {mutable_key: mutable_value.copy()} - db.update(**update_data) - - update_data[mutable_key].append("new_value_attempt") - assert ( - db.get(mutable_key) == mutable_value - ), "The database has been altered indirectly, by updating the item passed via the `update` method!" - - @pytest.mark.parametrize( - "replacement_value, expected_replacement", - ( - (132, 132), - ("test", "test"), - (set("132"), ("1", "2", "3")), - ({"132"}, ("132",)), - (frozenset("231"), ("1", "2", "3")), - (frozenset({"231"}), ("231",)), - (("1", "3", "2"), ("1", "3", "2")), - (["1", "5", "3"], ["1", "5", "3"]), - ), - ) - @pytest.mark.parametrize( - "setup_data, cross_period_persisted_keys", - ( - (dict(), frozenset()), - ({"test": [["test"]]}, frozenset()), - ({"test": [["test"]]}, frozenset({"test"})), - ({"test": ["test"]}, frozenset({"test"})), - ), - ) - def test_create( - self, - replacement_value: Any, - expected_replacement: Any, - setup_data: Dict, - cross_period_persisted_keys: FrozenSet[str], - ) -> None: - """Test `create` db.""" - db = AbciAppDB(setup_data) - db._cross_period_persisted_keys = cross_period_persisted_keys - db.create() - assert db._data == { - 0: setup_data, - 1: setup_data if cross_period_persisted_keys else {}, - }, "`create` did not produce the expected result!" - - mutable_key = "mutable" - mutable_value = ["test"] - existing_key = "test" - create_data = { - mutable_key: mutable_value.copy(), - existing_key: replacement_value, - } - db.create(**create_data) - - assert db._data == { - 0: setup_data, - 1: setup_data if cross_period_persisted_keys else {}, - 2: db.data_to_lists( - { - mutable_key: mutable_value.copy(), - existing_key: expected_replacement, - } - ), - }, "`create` did not produce the expected result!" - - create_data[mutable_key].append("new_value_attempt") - assert ( - db.get(mutable_key) == mutable_value - ), "The database has been altered indirectly, by updating the item passed via the `create` method!" - - def test_create_key_not_in_db(self) -> None: - """Test the `create` method when a given or a cross-period key does not exist in the db.""" - existing_key = "existing_key" - non_existing_key = "non_existing_key" - - db = AbciAppDB({existing_key: ["test_value"]}) - db._cross_period_persisted_keys = frozenset({non_existing_key}) - with pytest.raises( - ABCIAppInternalError, - match=f"Cross period persisted key `{non_existing_key}` " - "was not found in the db but was required for the next period.", - ): - db.create() - - db._cross_period_persisted_keys = frozenset({existing_key}) - db.create(**{non_existing_key: "test_value"}) - - @pytest.mark.parametrize( - "existing_data, cleanup_history_depth, cleanup_history_depth_current, expected", - ( - ( - {1: {"test": ["test", ["dummy_value1", "dummy_value2"]]}}, - 0, - None, - {1: {"test": ["test", ["dummy_value1", "dummy_value2"]]}}, - ), - ( - { - 1: {"test": ["test", ["dummy_value1", "dummy_value2"]]}, - 2: {"test": [0]}, - }, - 0, - None, - {2: {"test": [0]}}, - ), - ( - { - 1: {"test": ["test", ["dummy_value1", "dummy_value2"]]}, - 2: {"test": [0, 1, 2]}, - }, - 0, - 0, - {2: {"test": [0, 1, 2]}}, - ), - ( - { - 1: {"test": ["test", ["dummy_value1", "dummy_value2"]]}, - 2: {"test": [0, 1, 2]}, - }, - 0, - 1, - {2: {"test": [2]}}, - ), - ( - { - 1: {"test": ["test", ["dummy_value1", "dummy_value2"]]}, - 2: {"test": list(range(5))}, - 3: {"test": list(range(5, 10))}, - 4: {"test": list(range(10, 15))}, - 5: {"test": list(range(15, 20))}, - }, - 3, - 0, - { - 3: {"test": list(range(5, 10))}, - 4: {"test": list(range(10, 15))}, - 5: {"test": list(range(15, 20))}, - }, - ), - ( - { - 1: {"test": ["test", ["dummy_value1", "dummy_value2"]]}, - 2: {"test": list(range(5))}, - 3: {"test": list(range(5, 10))}, - 4: {"test": list(range(10, 15))}, - 5: {"test": list(range(15, 20))}, - }, - 5, - 3, - { - 1: {"test": ["test", ["dummy_value1", "dummy_value2"]]}, - 2: {"test": list(range(5))}, - 3: {"test": list(range(5, 10))}, - 4: {"test": list(range(10, 15))}, - 5: {"test": list(range(15 + 2, 20))}, - }, - ), - ( - { - 1: {"test": ["test", ["dummy_value1", "dummy_value2"]]}, - 2: {"test": list(range(5))}, - 3: {"test": list(range(5, 10))}, - 4: {"test": list(range(10, 15))}, - 5: {"test": list(range(15, 20))}, - }, - 2, - 3, - { - 4: {"test": list(range(10, 15))}, - 5: {"test": list(range(15 + 2, 20))}, - }, - ), - ( - { - 1: {"test": ["test", ["dummy_value1", "dummy_value2"]]}, - 2: {"test": list(range(5))}, - 3: {"test": list(range(5, 10))}, - 4: {"test": list(range(10, 15))}, - 5: {"test": list(range(15, 20))}, - }, - 0, - 1, - { - 5: {"test": [19]}, - }, - ), - ), - ) - def test_cleanup( - self, - existing_data: Dict[int, Dict[str, List[Any]]], - cleanup_history_depth: int, - cleanup_history_depth_current: Optional[int], - expected: Dict[int, Dict[str, List[Any]]], - ) -> None: - """Test cleanup db.""" - db = AbciAppDB({}) - db._cross_period_persisted_keys = frozenset() - for _, _data in existing_data.items(): - db._create_from_keys(**_data) - - db.cleanup(cleanup_history_depth, cleanup_history_depth_current) - assert db._data == expected - - def test_serialize(self) -> None: - """Test `serialize` method.""" - assert ( - self.db.serialize() - == '{"db_data": {"0": {"participants": [["a", "b"]]}}, "slashing_config": ""}' - ) - - @pytest.mark.parametrize( - "_data", - ({"db_data": {0: {"test": [0]}}, "slashing_config": "serialized_config"},), - ) - def test_sync(self, _data: Dict[str, Dict[int, Dict[str, List[Any]]]]) -> None: - """Test `sync` method.""" - try: - serialized_data = json.dumps(_data) - except TypeError as exc: - raise AssertionError( - "Incorrectly parametrized test. Data must be json serializable." - ) from exc - - self.db.sync(serialized_data) - assert self.db._data == _data["db_data"] - assert self.db.slashing_config == _data["slashing_config"] - - @pytest.mark.parametrize( - "serialized_data, match", - ( - (b"", "Could not decode data using "), - ( - json.dumps({"both_mandatory_keys_missing": {}}), - "internal error: Mandatory keys `db_data`, `slashing_config` are missing from the deserialized data: " - "{'both_mandatory_keys_missing': {}}\nThe following serialized data were given: " - '{"both_mandatory_keys_missing": {}}', - ), - ( - json.dumps({"db_data": {}}), - "internal error: Mandatory keys `db_data`, `slashing_config` are missing from the deserialized data: " - "{'db_data': {}}\nThe following serialized data were given: {\"db_data\": {}}", - ), - ( - json.dumps({"slashing_config": {}}), - "internal error: Mandatory keys `db_data`, `slashing_config` are missing from the deserialized data: " - "{'slashing_config': {}}\nThe following serialized data were given: {\"slashing_config\": {}}", - ), - ( - json.dumps( - {"db_data": {"invalid_index": {}}, "slashing_config": "anything"} - ), - "An invalid index was found while trying to sync the db using data: ", - ), - ( - json.dumps({"db_data": "invalid", "slashing_config": "anything"}), - "Could not decode db data with an invalid format: ", - ), - ), - ) - def test_sync_incorrect_data(self, serialized_data: Any, match: str) -> None: - """Test `sync` method with incorrect data.""" - with pytest.raises( - ABCIAppInternalError, - match=match, - ): - self.db.sync(serialized_data) - - def test_hash(self) -> None: - """Test `hash` method.""" - expected_hash = ( - b"\xd0^\xb0\x85\xf1\xf5\xd2\xe8\xe8\x85\xda\x1a\x99k" - b"\x1c\xde\xfa1\x8a\x87\xcc\xd7q?\xdf\xbbofz\xfb\x7fI" - ) - assert self.db.hash() == expected_hash - - -class TestBaseSynchronizedData: - """Test 'BaseSynchronizedData' class.""" - - def setup(self) -> None: - """Set up the tests.""" - self.participants = ("a", "b") - self.base_synchronized_data = BaseSynchronizedData( - db=AbciAppDB(setup_data=dict(participants=[self.participants])) - ) - - @given(text()) - def test_slashing_config(self, slashing_config: str) -> None: - """Test the `slashing_config` property.""" - self.base_synchronized_data.slashing_config = slashing_config - assert ( - self.base_synchronized_data.slashing_config - == self.base_synchronized_data.db.slashing_config - == slashing_config - ) - - def test_participants_getter_positive(self) -> None: - """Test 'participants' property getter.""" - assert frozenset(self.participants) == self.base_synchronized_data.participants - - def test_nb_participants_getter(self) -> None: - """Test 'participants' property getter.""" - assert len(self.participants) == self.base_synchronized_data.nb_participants - - def test_participants_getter_negative(self) -> None: - """Test 'participants' property getter, negative case.""" - base_synchronized_data = BaseSynchronizedData(db=AbciAppDB(setup_data={})) - # with pytest.raises(ValueError, match="Value of key=participants is None"): - with pytest.raises( - ValueError, - match=re.escape( - "'participants' field is not set for this period [0] and no default value was provided." - ), - ): - base_synchronized_data.participants - - def test_update(self) -> None: - """Test the 'update' method.""" - participants = ("a",) - expected = BaseSynchronizedData( - db=AbciAppDB(setup_data=dict(participants=[participants])) - ) - actual = self.base_synchronized_data.update(participants=participants) - assert expected.participants == actual.participants - assert actual.db._data == {0: {"participants": [("a", "b"), ("a",)]}} - - def test_create(self) -> None: - """Test the 'create' method.""" - self.base_synchronized_data.db._cross_period_persisted_keys = frozenset( - {"participants"} - ) - assert self.base_synchronized_data.db._data == { - 0: {"participants": [("a", "b")]} - } - actual = self.base_synchronized_data.create() - assert actual.db._data == { - 0: {"participants": [("a", "b")]}, - 1: {"participants": [("a", "b")]}, - } - - def test_repr(self) -> None: - """Test the '__repr__' magic method.""" - actual_repr = repr(self.base_synchronized_data) - expected_repr_regex = r"BaseSynchronizedData\(db=AbciAppDB\({(.*)}\)\)" - assert re.match(expected_repr_regex, actual_repr) is not None - - def test_participants_list_is_empty( - self, - ) -> None: - """Tets when participants list is set to zero.""" - base_synchronized_data = BaseSynchronizedData( - db=AbciAppDB(setup_data=dict(participants=[tuple()])) - ) - with pytest.raises(ValueError, match="List participants cannot be empty."): - _ = base_synchronized_data.participants - - def test_all_participants_list_is_empty( - self, - ) -> None: - """Tets when participants list is set to zero.""" - base_synchronized_data = BaseSynchronizedData( - db=AbciAppDB(setup_data=dict(all_participants=[tuple()])) - ) - with pytest.raises(ValueError, match="List participants cannot be empty."): - _ = base_synchronized_data.all_participants - - @pytest.mark.parametrize( - "n_participants, given_threshold, expected_threshold", - ( - (1, None, 1), - (5, None, 4), - (10, None, 7), - (345, None, 231), - (246236, None, 164158), - (1, 1, 1), - (5, 5, 5), - (10, 7, 7), - (10, 8, 8), - (10, 9, 9), - (10, 10, 10), - (345, 300, 300), - (246236, 194158, 194158), - ), - ) - def test_consensus_threshold( - self, n_participants: int, given_threshold: int, expected_threshold: int - ) -> None: - """Test the `consensus_threshold` property.""" - base_synchronized_data = BaseSynchronizedData( - db=AbciAppDB( - setup_data=dict( - all_participants=[tuple(range(n_participants))], - consensus_threshold=[given_threshold], - ) - ) - ) - - assert base_synchronized_data.consensus_threshold == expected_threshold - - @pytest.mark.parametrize( - "n_participants, given_threshold", - ( - (1, 2), - (5, 2), - (10, 4), - (10, 11), - (10, 18), - (345, 200), - (246236, 164157), - (246236, 246237), - ), - ) - def test_consensus_threshold_incorrect( - self, - n_participants: int, - given_threshold: int, - ) -> None: - """Test the `consensus_threshold` property when an incorrect threshold value has been inserted to the db.""" - base_synchronized_data = BaseSynchronizedData( - db=AbciAppDB( - setup_data=dict( - all_participants=[tuple(range(n_participants))], - consensus_threshold=[given_threshold], - ) - ) - ) - - with pytest.raises(ValueError, match="Consensus threshold "): - _ = base_synchronized_data.consensus_threshold - - def test_properties(self) -> None: - """Test several properties""" - participants = ["b", "a"] - randomness_str = ( - "3439d92d58e47d342131d446a3abe264396dd264717897af30525c98408c834f" - ) - randomness_value = 0.20400769574270503 - most_voted_keeper_address = "most_voted_keeper_address" - blacklisted_keepers = "blacklisted_keepers" - participant_to_selection = participant_to_randomness = participant_to_votes = { - "sender": DummyPayload(sender="sender", dummy_attribute=0) - } - safe_contract_address = "0x0" - - base_synchronized_data = BaseSynchronizedData( - db=AbciAppDB( - setup_data=AbciAppDB.data_to_lists( - dict( - participants=participants, - all_participants=participants, - most_voted_randomness=randomness_str, - most_voted_keeper_address=most_voted_keeper_address, - blacklisted_keepers=blacklisted_keepers, - participant_to_selection=CollectionRound.serialize_collection( - participant_to_selection - ), - participant_to_randomness=CollectionRound.serialize_collection( - participant_to_randomness - ), - participant_to_votes=CollectionRound.serialize_collection( - participant_to_votes - ), - safe_contract_address=safe_contract_address, - ) - ) - ) - ) - assert self.base_synchronized_data.period_count == 0 - assert base_synchronized_data.all_participants == frozenset(participants) - assert base_synchronized_data.sorted_participants == ["a", "b"] - assert base_synchronized_data.max_participants == len(participants) - assert abs(base_synchronized_data.keeper_randomness - randomness_value) < 1e-10 - assert base_synchronized_data.most_voted_randomness == randomness_str - assert ( - base_synchronized_data.most_voted_keeper_address - == most_voted_keeper_address - ) - assert base_synchronized_data.is_keeper_set - assert base_synchronized_data.blacklisted_keepers == {blacklisted_keepers} - assert ( - base_synchronized_data.participant_to_selection == participant_to_selection - ) - assert ( - base_synchronized_data.participant_to_randomness - == participant_to_randomness - ) - assert base_synchronized_data.participant_to_votes == participant_to_votes - assert base_synchronized_data.safe_contract_address == safe_contract_address - - -class DummyConcreteRound(AbstractRound): - """A dummy concrete round's implementation.""" - - payload_class: Optional[Type[BaseTxPayload]] = None - synchronized_data_class = MagicMock() - payload_attribute = MagicMock() - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, EventType]]: - """A dummy `end_block` implementation.""" - - def check_payload(self, payload: BaseTxPayload) -> None: - """A dummy `check_payload` implementation.""" - - def process_payload(self, payload: BaseTxPayload) -> None: - """A dummy `process_payload` implementation.""" - - -class TestAbstractRound: - """Test the 'AbstractRound' class.""" - - def setup(self) -> None: - """Set up the tests.""" - self.known_payload_type = ConcreteRoundA.payload_class - self.participants = ("a", "b") - self.base_synchronized_data = BaseSynchronizedData( - db=AbciAppDB( - setup_data=dict( - all_participants=[self.participants], - participants=[self.participants], - consensus_threshold=[2], - ) - ) - ) - self.round = ConcreteRoundA(self.base_synchronized_data, MagicMock()) - - def test_auto_round_id(self) -> None: - """Test that the 'auto_round_id()' method works as expected.""" - - assert DummyConcreteRound.auto_round_id() == "dummy_concrete_round" - - def test_must_not_set_round_id(self) -> None: - """Test that the 'round_id' must be set in concrete classes.""" - - # no exception as round id is auto-assigned - my_concrete_round = DummyConcreteRound(MagicMock(), MagicMock()) - assert my_concrete_round.round_id == "dummy_concrete_round" - - def test_must_set_payload_class_type(self) -> None: - """Test that the 'payload_class' must be set in concrete classes.""" - - with pytest.raises( - AbstractRoundInternalError, match="'payload_class' not set on .*" - ): - - class MyConcreteRound(AbstractRound): - synchronized_data_class = MagicMock() - payload_attribute = MagicMock() - # here payload_class is missing - - def test_check_payload_type_with_previous_round_transaction(self) -> None: - """Test check 'check_payload_type'.""" - - class MyConcreteRound(DummyConcreteRound): - """A concrete round with the payload class defined.""" - - payload_class = BaseTxPayload - - with pytest.raises(LateArrivingTransaction): - MyConcreteRound(MagicMock(), MagicMock(), BaseTxPayload).check_payload_type( - MagicMock(payload=BaseTxPayload("dummy")) - ) - - def test_check_payload_type(self) -> None: - """Test check 'check_payload_type'.""" - - with pytest.raises( - TransactionTypeNotRecognizedError, - match="current round does not allow transactions", - ): - DummyConcreteRound(MagicMock(), MagicMock()).check_payload_type(MagicMock()) - - def test_synchronized_data_getter(self) -> None: - """Test 'synchronized_data' property getter.""" - state = self.round.synchronized_data - assert state.participants == frozenset(self.participants) - - def test_check_transaction_unknown_payload(self) -> None: - """Test 'check_transaction' method, with unknown payload type.""" - tx_type = "unknown_payload" - tx_mock = MagicMock() - tx_mock.payload_class = tx_type - with pytest.raises( - TransactionTypeNotRecognizedError, - match="request '.*' not recognized", - ): - self.round.check_transaction(tx_mock) - - def test_check_transaction_known_payload(self) -> None: - """Test 'check_transaction' method, with known payload type.""" - tx_mock = MagicMock() - tx_mock.payload = self.known_payload_type(sender="dummy") - self.round.check_transaction(tx_mock) - - def test_process_transaction_negative_unknown_payload(self) -> None: - """Test 'process_transaction' method, with unknown payload type.""" - tx_mock = MagicMock() - tx_mock.payload = object - with pytest.raises( - TransactionTypeNotRecognizedError, - match="request '.*' not recognized", - ): - self.round.process_transaction(tx_mock) - - def test_process_transaction_negative_check_transaction_fails(self) -> None: - """Test 'process_transaction' method, with 'check_transaction' failing.""" - tx_mock = MagicMock() - tx_mock.payload = object - error_message = "transaction not valid" - with mock.patch.object( - self.round, "check_payload_type", side_effect=ValueError(error_message) - ): - with pytest.raises(ValueError, match=error_message): - self.round.process_transaction(tx_mock) - - def test_process_transaction_positive(self) -> None: - """Test 'process_transaction' method, positive case.""" - tx_mock = MagicMock() - tx_mock.payload = BaseTxPayload(sender="dummy") - self.round.process_transaction(tx_mock) - - def test_check_majority_possible_raises_error_when_nb_participants_is_0( - self, - ) -> None: - """Check that 'check_majority_possible' raises error when nb_participants=0.""" - with pytest.raises( - ABCIAppInternalError, - match="nb_participants not consistent with votes_by_participants", - ): - DummyConcreteRound( - self.base_synchronized_data, MagicMock() - ).check_majority_possible({}, 0) - - def test_check_majority_possible_passes_when_vote_set_is_empty(self) -> None: - """Check that 'check_majority_possible' passes when the set of votes is empty.""" - DummyConcreteRound( - self.base_synchronized_data, MagicMock() - ).check_majority_possible({}, 1) - - def test_check_majority_possible_passes_when_vote_set_nonempty_and_check_passes( - self, - ) -> None: - """ - Check that 'check_majority_possible' passes when set of votes is non-empty. - - The check passes because: - - the threshold is 2 - - the other voter can vote for the same item of the first voter - """ - DummyConcreteRound( - self.base_synchronized_data, MagicMock() - ).check_majority_possible({"alice": DummyPayload("alice", True)}, 2) - - def test_check_majority_possible_passes_when_payload_attributes_majority_match( - self, - ) -> None: - """ - Test 'check_majority_possible' when set of votes is non-empty and the majority of the attribute values match. - - The check passes because: - - the threshold is 3 (participants are 4) - - 3 voters have the same attribute value in their payload - """ - DummyConcreteRound( - self.base_synchronized_data, MagicMock() - ).check_majority_possible( - { - "voter_1": DummyPayload("voter_1", 0), - "voter_2": DummyPayload("voter_2", 0), - "voter_3": DummyPayload("voter_3", 0), - }, - 4, - ) - - def test_check_majority_possible_passes_when_vote_set_nonempty_and_check_doesnt_pass( - self, - ) -> None: - """ - Check that 'check_majority_possible' doesn't pass when set of votes is non-empty. - - the check does not pass because: - - the threshold is 2 - - both voters have already voted for different items - """ - with pytest.raises( - ABCIAppException, - match="cannot reach quorum=2, number of remaining votes=0, number of most voted item's votes=1", - ): - DummyConcreteRound( - self.base_synchronized_data, MagicMock() - ).check_majority_possible( - { - "alice": DummyPayload("alice", False), - "bob": DummyPayload("bob", True), - }, - 2, - ) - - def test_is_majority_possible_positive_case(self) -> None: - """Test 'is_majority_possible', positive case.""" - assert DummyConcreteRound( - self.base_synchronized_data, MagicMock() - ).is_majority_possible({"alice": DummyPayload("alice", False)}, 2) - - def test_is_majority_possible_negative_case(self) -> None: - """Test 'is_majority_possible', negative case.""" - assert not DummyConcreteRound( - self.base_synchronized_data, MagicMock() - ).is_majority_possible( - { - "alice": DummyPayload("alice", False), - "bob": DummyPayload("bob", True), - }, - 2, - ) - - def test_check_majority_possible_raises_error_when_new_voter_already_voted( - self, - ) -> None: - """Test 'check_majority_possible_with_new_vote' raises when new voter already voted.""" - with pytest.raises(ABCIAppInternalError, match="voter has already voted"): - DummyConcreteRound( - self.base_synchronized_data, - MagicMock(), - ).check_majority_possible_with_new_voter( - {"alice": DummyPayload("alice", False)}, - "alice", - DummyPayload("alice", True), - 2, - ) - - def test_check_majority_possible_raises_error_when_nb_participants_inconsistent( - self, - ) -> None: - """Test 'check_majority_possible_with_new_vote' raises when 'nb_participants' inconsistent with other args.""" - with pytest.raises( - ABCIAppInternalError, - match="nb_participants not consistent with votes_by_participants", - ): - DummyConcreteRound( - self.base_synchronized_data, - MagicMock(), - ).check_majority_possible_with_new_voter( - {"alice": DummyPayload("alice", True)}, - "bob", - DummyPayload("bob", True), - 1, - ) - - def test_check_majority_possible_when_check_passes( - self, - ) -> None: - """ - Test 'check_majority_possible_with_new_vote' when the check passes. - - The test passes because: - - the number of participants is 2, and so the threshold is 2 - - the new voter votes for the same item already voted by voter 1. - """ - DummyConcreteRound( - self.base_synchronized_data, - MagicMock(), - ).check_majority_possible_with_new_voter( - {"alice": DummyPayload("alice", True)}, "bob", DummyPayload("bob", True), 2 - ) - - -class TestTimeouts: - """Test the 'Timeouts' class.""" - - def setup(self) -> None: - """Set up the test.""" - self.timeouts: Timeouts = Timeouts() - - def test_size(self) -> None: - """Test the 'size' property.""" - assert self.timeouts.size == 0 - self.timeouts._heap.append(MagicMock()) - assert self.timeouts.size == 1 - - def test_add_timeout(self) -> None: - """Test the 'add_timeout' method.""" - # the first time, entry_count = 0 - entry_count = self.timeouts.add_timeout(datetime.datetime.now(), MagicMock()) - assert entry_count == 0 - - # the second time, entry_count is incremented - entry_count = self.timeouts.add_timeout(datetime.datetime.now(), MagicMock()) - assert entry_count == 1 - - def test_cancel_timeout(self) -> None: - """Test the 'cancel_timeout' method.""" - entry_count = self.timeouts.add_timeout(datetime.datetime.now(), MagicMock()) - assert self.timeouts.size == 1 - - self.timeouts.cancel_timeout(entry_count) - - # cancelling timeouts does not remove them from the heap - assert self.timeouts.size == 1 - - def test_pop_earliest_cancelled_timeouts(self) -> None: - """Test the 'pop_earliest_cancelled_timeouts' method.""" - entry_count_1 = self.timeouts.add_timeout(datetime.datetime.now(), MagicMock()) - entry_count_2 = self.timeouts.add_timeout(datetime.datetime.now(), MagicMock()) - self.timeouts.cancel_timeout(entry_count_1) - self.timeouts.cancel_timeout(entry_count_2) - self.timeouts.pop_earliest_cancelled_timeouts() - assert self.timeouts.size == 0 - - def test_get_earliest_timeout_a(self) -> None: - """Test the 'get_earliest_timeout' method.""" - deadline_1 = datetime.datetime.now() - event_1 = MagicMock() - - sleep(0.5) - - deadline_2 = datetime.datetime.now() - event_2 = MagicMock() - assert deadline_1 < deadline_2 - - self.timeouts.add_timeout(deadline_2, event_2) - self.timeouts.add_timeout(deadline_1, event_1) - - assert self.timeouts.size == 2 - # test that we get the event with the earliest deadline - timeout, event = self.timeouts.get_earliest_timeout() - assert timeout == deadline_1 - assert event == event_1 - - # test that get_earliest_timeout does not remove elements - assert self.timeouts.size == 2 - - popped_timeout, popped_event = self.timeouts.pop_timeout() - assert popped_timeout == timeout - assert popped_event == event - - def test_get_earliest_timeout_b(self) -> None: - """Test the 'get_earliest_timeout' method.""" - - deadline_1 = datetime.datetime.now() - event_1 = MagicMock() - - sleep(0.5) - - deadline_2 = datetime.datetime.now() - event_2 = MagicMock() - assert deadline_1 < deadline_2 - - self.timeouts.add_timeout(deadline_1, event_1) - self.timeouts.add_timeout(deadline_2, event_2) - - assert self.timeouts.size == 2 - # test that we get the event with the earliest deadline - timeout, event = self.timeouts.get_earliest_timeout() - assert timeout == deadline_1 - assert event == event_1 - - # test that get_earliest_timeout does not remove elements - assert self.timeouts.size == 2 - - def test_pop_timeout(self) -> None: - """Test the 'pop_timeout' method.""" - deadline_1 = datetime.datetime.now() - event_1 = MagicMock() - - sleep(0.5) - - deadline_2 = datetime.datetime.now() - event_2 = MagicMock() - assert deadline_1 < deadline_2 - - self.timeouts.add_timeout(deadline_2, event_2) - self.timeouts.add_timeout(deadline_1, event_1) - - assert self.timeouts.size == 2 - # test that we get the event with the earliest deadline - timeout, event = self.timeouts.pop_timeout() - assert timeout == deadline_1 - assert event == event_1 - - # test that pop_timeout removes elements - assert self.timeouts.size == 1 - - -STUB_TERMINATION_CONFIG = abci_base.BackgroundAppConfig( - round_cls=ConcreteBackgroundRound, - start_event=ConcreteEvents.TERMINATE, - abci_app=TerminationAppTest, -) - -STUB_SLASH_CONFIG = abci_base.BackgroundAppConfig( - round_cls=ConcreteBackgroundSlashingRound, - start_event=ConcreteEvents.SLASH_START, - end_event=ConcreteEvents.SLASH_END, - abci_app=SlashingAppTest, -) - - -class TestAbciApp: - """Test the 'AbciApp' class.""" - - def setup(self) -> None: - """Set up the test.""" - self.abci_app = AbciAppTest(MagicMock(), MagicMock(), MagicMock()) - self.abci_app.add_background_app(STUB_TERMINATION_CONFIG) - - def teardown(self) -> None: - """Teardown the test.""" - self.abci_app.background_apps.clear() - - @pytest.mark.parametrize("flag", (True, False)) - def test_is_abstract(self, flag: bool) -> None: - """Test `is_abstract` property.""" - - class CopyOfAbciApp(AbciAppTest): - """Copy to avoid side effects due to state change""" - - CopyOfAbciApp._is_abstract = flag - assert CopyOfAbciApp.is_abstract() is flag - - def test_initial_round_cls_not_set(self) -> None: - """Test when 'initial_round_cls' is not set.""" - - with pytest.raises( - ABCIAppInternalError, match="'initial_round_cls' field not set" - ): - - class MyAbciApp(AbciApp): - # here 'initial_round_cls' should be defined. - # ... - transition_function: AbciAppTransitionFunction = {} - - def test_transition_function_not_set(self) -> None: - """Test when 'transition_function' is not set.""" - with pytest.raises( - ABCIAppInternalError, match="'transition_function' field not set" - ): - - class MyAbciApp(AbciApp): - initial_round_cls = ConcreteRoundA - # here 'transition_function' should be defined. - # ... - - def test_last_timestamp_negative(self) -> None: - """Test the 'last_timestamp' property, negative case.""" - with pytest.raises(ABCIAppInternalError, match="last timestamp is None"): - self.abci_app.last_timestamp - - def test_last_timestamp_positive(self) -> None: - """Test the 'last_timestamp' property, positive case.""" - expected = MagicMock() - self.abci_app._last_timestamp = expected - assert expected == self.abci_app.last_timestamp - - @pytest.mark.parametrize( - "db_key, sync_classes, default, property_found", - ( - ("", set(), "default", False), - ("non_existing_key", {BaseSynchronizedData}, True, False), - ("participants", {BaseSynchronizedData}, {}, False), - ("is_keeper_set", {BaseSynchronizedData}, True, True), - ), - ) - def test_get_synced_value( - self, - db_key: str, - sync_classes: Set[Type[BaseSynchronizedData]], - default: Any, - property_found: bool, - ) -> None: - """Test the `_get_synced_value` method.""" - res = self.abci_app._get_synced_value(db_key, sync_classes, default) - if property_found: - assert res == getattr(self.abci_app.synchronized_data, db_key) - return - assert res == self.abci_app.synchronized_data.db.get(db_key, default) - - def test_process_event(self) -> None: - """Test the 'process_event' method, positive case, with timeout events.""" - self.abci_app.add_background_app(STUB_SLASH_CONFIG) - self.abci_app.setup() - self.abci_app._last_timestamp = MagicMock() - assert self.abci_app._transition_backup.transition_function is None - assert isinstance(self.abci_app.current_round, ConcreteRoundA) - self.abci_app.process_event(ConcreteEvents.B) - assert isinstance(self.abci_app.current_round, ConcreteRoundB) - self.abci_app.process_event(ConcreteEvents.TIMEOUT) - assert isinstance(self.abci_app.current_round, ConcreteRoundA) - self.abci_app.process_event(ConcreteEvents.TERMINATE) - assert isinstance(self.abci_app.current_round, ConcreteTerminationRoundA) - expected_backup = deepcopy(self.abci_app.transition_function) - assert ( - self.abci_app._transition_backup.transition_function - == AbciAppTest.transition_function - ) - self.abci_app.process_event(ConcreteEvents.SLASH_START) - assert isinstance(self.abci_app.current_round, ConcreteSlashingRoundA) - assert ( - self.abci_app._transition_backup.transition_function - == expected_backup - == TerminationAppTest.transition_function - ) - assert self.abci_app.transition_function == SlashingAppTest.transition_function - self.abci_app.process_event(ConcreteEvents.SLASH_END) - # should return back to the round that was running before the slashing started - assert isinstance(self.abci_app.current_round, ConcreteTerminationRoundA) - assert self.abci_app.transition_function == expected_backup - assert self.abci_app._transition_backup.transition_function is None - assert self.abci_app._transition_backup.round is None - - def test_process_event_negative_case(self) -> None: - """Test the 'process_event' method, negative case.""" - with mock.patch.object(self.abci_app.logger, "warning") as mock_warning: - self.abci_app.process_event(ConcreteEvents.A) - mock_warning.assert_called_with( - "Cannot process event 'a' as current state is not set" - ) - - def test_update_time(self) -> None: - """Test the 'update_time' method.""" - # schedule round_a - current_time = datetime.datetime.now() - self.abci_app.setup() - self.abci_app._last_timestamp = current_time - - # move to round_b that schedules timeout events - self.abci_app.process_event(ConcreteEvents.B) - assert self.abci_app.current_round_id == "concrete_round_b" - - # simulate most recent timestamp beyond earliest deadline - # after pop, len(timeouts) == 0, because round_a does not schedule new timeout events - current_time = current_time + datetime.timedelta(0, AbciAppTest.TIMEOUT) - self.abci_app.update_time(current_time) - - # now we are back to round_a - assert self.abci_app.current_round_id == "concrete_round_a" - - # move to round_c that schedules timeout events to itself - self.abci_app.process_event(ConcreteEvents.C) - assert self.abci_app.current_round_id == "concrete_round_c" - - # simulate most recent timestamp beyond earliest deadline - # after pop, len(timeouts) == 0, because round_c schedules timeout events - current_time = current_time + datetime.timedelta(0, AbciAppTest.TIMEOUT) - self.abci_app.update_time(current_time) - - assert self.abci_app.current_round_id == "concrete_round_c" - - # further update changes nothing - height = self.abci_app.current_round_height - self.abci_app.update_time(current_time) - assert height == self.abci_app.current_round_height - - def test_get_all_events(self) -> None: - """Test the all events getter.""" - assert { - ConcreteEvents.A, - ConcreteEvents.B, - ConcreteEvents.C, - ConcreteEvents.TIMEOUT, - } == self.abci_app.get_all_events() - - @pytest.mark.parametrize("include_background_rounds", (True, False)) - def test_get_all_rounds_classes( - self, - include_background_rounds: bool, - ) -> None: - """Test the get all rounds getter.""" - expected_rounds = {ConcreteRoundA, ConcreteRoundB, ConcreteRoundC} - - if include_background_rounds: - expected_rounds.update( - { - ConcreteBackgroundRound, - ConcreteTerminationRoundA, - ConcreteTerminationRoundB, - ConcreteTerminationRoundC, - } - ) - - actual_rounds = self.abci_app.get_all_round_classes( - {ConcreteBackgroundRound}, include_background_rounds - ) - - assert actual_rounds == expected_rounds - - def test_get_all_rounds_classes_bg_ever_running( - self, - ) -> None: - """Test the get all rounds when the background round is of an ever running type.""" - # we clear the pre-existing bg apps and add an ever running - self.abci_app.background_apps.clear() - self.abci_app.add_background_app( - abci_base.BackgroundAppConfig(ConcreteBackgroundRound) - ) - include_background_rounds = True - expected_rounds = { - ConcreteRoundA, - ConcreteRoundB, - ConcreteRoundC, - } - assert expected_rounds == self.abci_app.get_all_round_classes( - {ConcreteBackgroundRound}, include_background_rounds - ) - - def test_add_background_app(self) -> None: - """Tests the add method for the background apps.""" - # remove the terminating bg round added in `setup()` and the pending offences bg app added in the metaclass - self.abci_app.background_apps.clear() - - class EmptyAbciApp(AbciAppTest): - """An AbciApp without background apps' attributes set.""" - - cross_period_persisted_keys = frozenset({"1", "2"}) - - class BackgroundAbciApp(AbciAppTest): - """A mock background AbciApp.""" - - cross_period_persisted_keys = frozenset({"2", "3"}) - - assert len(EmptyAbciApp.background_apps) == 0 - assert EmptyAbciApp.cross_period_persisted_keys == {"1", "2"} - # add the background app - bg_app_config = abci_base.BackgroundAppConfig( - round_cls=ConcreteBackgroundRound, - start_event=ConcreteEvents.TERMINATE, - abci_app=BackgroundAbciApp, - ) - EmptyAbciApp.add_background_app(bg_app_config) - assert len(EmptyAbciApp.background_apps) == 1 - assert EmptyAbciApp.cross_period_persisted_keys == {"1", "2", "3"} - - def test_cleanup(self) -> None: - """Test the cleanup method.""" - self.abci_app.setup() - - # Dummy parameters, synchronized data and round - cleanup_history_depth = 1 - start_history_depth = 5 - max_participants = 4 - dummy_synchronized_data = BaseSynchronizedData( - db=AbciAppDB(setup_data=dict(participants=[max_participants])) - ) - dummy_round = ConcreteRoundA(dummy_synchronized_data, MagicMock()) - - # Add dummy data - self.abci_app._previous_rounds = [dummy_round] * start_history_depth - self.abci_app._round_results = [dummy_synchronized_data] * start_history_depth - self.abci_app.synchronized_data.db._data = { - i: {"dummy_key": ["dummy_value"]} for i in range(start_history_depth) - } - - round_height = self.abci_app.current_round_height - # Verify that cleanup reduces the data amount - assert len(self.abci_app._previous_rounds) == start_history_depth - assert len(self.abci_app._round_results) == start_history_depth - assert len(self.abci_app.synchronized_data.db._data) == start_history_depth - assert list(self.abci_app.synchronized_data.db._data.keys()) == list( - range(start_history_depth) - ) - previous_reset_index = self.abci_app.synchronized_data.db.reset_index - - self.abci_app.cleanup(cleanup_history_depth) - - assert len(self.abci_app._previous_rounds) == cleanup_history_depth - assert len(self.abci_app._round_results) == cleanup_history_depth - assert len(self.abci_app.synchronized_data.db._data) == cleanup_history_depth - assert list(self.abci_app.synchronized_data.db._data.keys()) == list( - range(start_history_depth - cleanup_history_depth, start_history_depth) - ) - # reset_index must not change after a cleanup - assert self.abci_app.synchronized_data.db.reset_index == previous_reset_index - - # Verify round height stays unaffected - assert self.abci_app.current_round_height == round_height - - # Add more values to the history - reset_index = self.abci_app.synchronized_data.db.reset_index - cleanup_history_depth_current = 3 - for _ in range(10): - self.abci_app.synchronized_data.db.update(dummy_key="dummy_value") - - # Check that the history cleanup keeps the desired history length - self.abci_app.cleanup_current_histories(cleanup_history_depth_current) - history_len = len( - self.abci_app.synchronized_data.db._data[reset_index]["dummy_key"] - ) - assert history_len == cleanup_history_depth_current - - @mock.patch.object(ConcreteBackgroundRound, "check_transaction") - @pytest.mark.parametrize( - "transaction", - [mock.MagicMock(payload=DUMMY_CONCRETE_BACKGROUND_PAYLOAD)], - ) - def test_check_transaction_for_termination_round( - self, - check_transaction_mock: mock.Mock, - transaction: Transaction, - ) -> None: - """Tests process_transaction when it's a transaction meant for the termination app.""" - self.abci_app.setup() - self.abci_app.check_transaction(transaction) - check_transaction_mock.assert_called_with(transaction) - - @mock.patch.object(ConcreteBackgroundRound, "process_transaction") - @pytest.mark.parametrize( - "transaction", - [mock.MagicMock(payload=DUMMY_CONCRETE_BACKGROUND_PAYLOAD)], - ) - def test_process_transaction_for_termination_round( - self, - process_transaction_mock: mock.Mock, - transaction: Transaction, - ) -> None: - """Tests process_transaction when it's a transaction meant for the termination app.""" - self.abci_app.setup() - self.abci_app.process_transaction(transaction) - process_transaction_mock.assert_called_with(transaction) - - -class TestOffenceTypeFns: - """Test `OffenceType`-related functions.""" - - @staticmethod - def test_light_offences() -> None: - """Test `light_offences` function.""" - assert list(light_offences()) == [ - OffenseType.VALIDATOR_DOWNTIME, - OffenseType.INVALID_PAYLOAD, - OffenseType.BLACKLISTED, - OffenseType.SUSPECTED, - ] - - @staticmethod - def test_serious_offences() -> None: - """Test `serious_offences` function.""" - assert list(serious_offences()) == [ - OffenseType.UNKNOWN, - OffenseType.DOUBLE_SIGNING, - OffenseType.LIGHT_CLIENT_ATTACK, - ] - - -@composite -def availability_window_data(draw: DrawFn) -> Dict[str, int]: - """A strategy for building valid availability window data.""" - max_length = draw(integers(min_value=1, max_value=12_000)) - array = draw(integers(min_value=0, max_value=(2**max_length) - 1)) - num_positive = draw(integers(min_value=0, max_value=1_000_000)) - num_negative = draw(integers(min_value=0, max_value=1_000_000)) - - return { - "max_length": max_length, - "array": array, - "num_positive": num_positive, - "num_negative": num_negative, - } - - -class TestAvailabilityWindow: - """Test `AvailabilityWindow`.""" - - @staticmethod - @given(integers(min_value=1, max_value=100)) - def test_not_equal(max_length: int) -> None: - """Test the `add` method.""" - availability_window_1 = AvailabilityWindow(max_length) - availability_window_2 = AvailabilityWindow(max_length) - assert availability_window_1 == availability_window_2 - availability_window_2.add(False) - assert availability_window_1 != availability_window_2 - # test with a different type - assert availability_window_1 != MagicMock() - - @staticmethod - @given(integers(min_value=0, max_value=100), data()) - def test_add(max_length: int, hypothesis_data: Any) -> None: - """Test the `add` method.""" - if max_length < 1: - with pytest.raises( - ValueError, - match=f"An `AvailabilityWindow` with a `max_length` {max_length} < 1 is not valid.", - ): - AvailabilityWindow(max_length) - return - - availability_window = AvailabilityWindow(max_length) - - expected_positives = expected_negatives = 0 - for i in range(max_length): - value = hypothesis_data.draw(booleans()) - availability_window.add(value) - items_in = i + 1 - assert len(availability_window._window) == items_in - assert availability_window._window[-1] is value - expected_positives += 1 if value else 0 - assert availability_window._num_positive == expected_positives - expected_negatives = items_in - expected_positives - assert availability_window._num_negative == expected_negatives - - # max length is reached and window starts cycling - assert len(availability_window._window) == max_length - for _ in range(10): - value = hypothesis_data.draw(booleans()) - expected_popped_value = ( - None if max_length == 0 else availability_window._window[0] - ) - availability_window.add(value) - assert len(availability_window._window) == max_length - if expected_popped_value is not None: - expected_positives -= bool(expected_popped_value) - expected_negatives -= bool(not expected_popped_value) - expected_positives += bool(value) - expected_negatives += bool(not value) - assert availability_window._num_positive == expected_positives - assert availability_window._num_negative == expected_negatives - - @staticmethod - @given( - max_length=integers(min_value=1, max_value=30_000), - num_positive=integers(min_value=0), - num_negative=integers(min_value=0), - ) - @pytest.mark.parametrize( - "window, expected_serialization", - ( - (deque(()), 0), - (deque((False, False, False)), 0), - (deque((True, False, True)), 5), - (deque((True for _ in range(3))), 7), - ( - deque((True for _ in range(1000))), - int( - "10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958" - "58127594672917553146825187145285692314043598457757469857480393456777482423098542107460506237114187" - "79541821530464749835819412673987675591655439460770629145711964776865421676604298316526243868372056" - "68069375" - ), - ), - ), - ) - def test_to_dict( - max_length: int, - num_positive: int, - num_negative: int, - window: Deque, - expected_serialization: int, - ) -> None: - """Test `to_dict` method.""" - availability_window = AvailabilityWindow(max_length) - availability_window._num_positive = num_positive - availability_window._num_negative = num_negative - availability_window._window = window - assert availability_window.to_dict() == { - "max_length": max_length, - "array": expected_serialization, - "num_positive": num_positive, - "num_negative": num_negative, - } - - @staticmethod - @pytest.mark.parametrize( - "data_, key, validator, expected_error", - ( - ({"a": 1, "b": 2, "c": 3}, "a", lambda x: x > 0, None), - ( - {"a": 1, "b": 2, "c": 3}, - "d", - lambda x: x > 0, - r"Missing required key: d\.", - ), - ( - {"a": "1", "b": 2, "c": 3}, - "a", - lambda x: x > 0, - r"a must be of type int\.", - ), - ( - {"a": -1, "b": 2, "c": 3}, - "a", - lambda x: x > 0, - r"a has invalid value -1\.", - ), - ), - ) - def test_validate_key( - data_: dict, key: str, validator: Callable, expected_error: Optional[str] - ) -> None: - """Test the `_validate_key` method.""" - if expected_error: - with pytest.raises(ValueError, match=expected_error): - AvailabilityWindow._validate_key(data_, key, validator) - else: - AvailabilityWindow._validate_key(data_, key, validator) - - @staticmethod - @pytest.mark.parametrize( - "data_, error_regex", - ( - ("not a dict", r"Expected dict, got"), - ( - {"max_length": -1, "array": 42, "num_positive": 10, "num_negative": 0}, - r"max_length", - ), - ( - {"max_length": 2, "array": 4, "num_positive": 10, "num_negative": 0}, - r"array", - ), - ( - {"max_length": 8, "array": 42, "num_positive": -1, "num_negative": 0}, - r"num_positive", - ), - ( - {"max_length": 8, "array": 42, "num_positive": 10, "num_negative": -1}, - r"num_negative", - ), - ), - ) - def test_validate_negative(data_: dict, error_regex: str) -> None: - """Negative tests for the `_validate` method.""" - with pytest.raises((TypeError, ValueError), match=error_regex): - AvailabilityWindow._validate(data_) - - @staticmethod - @given(availability_window_data()) - def test_validate_positive(data_: Dict[str, int]) -> None: - """Positive tests for the `_validate` method.""" - AvailabilityWindow._validate(data_) - - @staticmethod - @given(availability_window_data()) - def test_from_dict(data_: Dict[str, int]) -> None: - """Test `from_dict` method.""" - availability_window = AvailabilityWindow.from_dict(data_) - - # convert the serialized array to a binary string - binary_number = bin(data_["array"])[2:] - # convert each character in the binary string to a flag - flags = [bool(int(digit)) for digit in binary_number] - expected_window = deque(flags, maxlen=data_["max_length"]) - - assert availability_window._max_length == data_["max_length"] - assert availability_window._window == expected_window - assert availability_window._num_positive == data_["num_positive"] - assert availability_window._num_negative == data_["num_negative"] - - @staticmethod - @given(availability_window_data()) - def test_to_dict_and_back(data_: Dict[str, int]) -> None: - """Test that the `from_dict` produces an object that generates the input data again when calling `to_dict`.""" - availability_window = AvailabilityWindow.from_dict(data_) - assert availability_window.to_dict() == data_ - - -class TestOffenceStatus: - """Test the `OffenceStatus` dataclass.""" - - @staticmethod - @pytest.mark.parametrize("custom_amount", (0, 5)) - @pytest.mark.parametrize("light_unit_amount, serious_unit_amount", ((1, 2),)) - @pytest.mark.parametrize( - "validator_downtime, invalid_payload, blacklisted, suspected, " - "num_unknown_offenses, num_double_signed, num_light_client_attack, expected", - ( - (False, False, False, False, 0, 0, 0, 0), - (True, False, False, False, 0, 0, 0, 1), - (False, True, False, False, 0, 0, 0, 1), - (False, False, True, False, 0, 0, 0, 1), - (False, False, False, True, 0, 0, 0, 1), - (False, False, False, False, 1, 0, 0, 2), - (False, False, False, False, 0, 1, 0, 2), - (False, False, False, False, 0, 0, 1, 2), - (False, False, False, False, 0, 2, 1, 6), - (False, True, False, True, 5, 2, 1, 18), - (True, True, True, True, 5, 2, 1, 20), - ), - ) - def test_slash_amount( - custom_amount: int, - light_unit_amount: int, - serious_unit_amount: int, - validator_downtime: bool, - invalid_payload: bool, - blacklisted: bool, - suspected: bool, - num_unknown_offenses: int, - num_double_signed: int, - num_light_client_attack: int, - expected: int, - ) -> None: - """Test the `slash_amount` method.""" - status = OffenceStatus() - - if validator_downtime: - for _ in range(abci_base.NUMBER_OF_BLOCKS_TRACKED): - status.validator_downtime.add(True) - - for _ in range(abci_base.NUMBER_OF_ROUNDS_TRACKED): - if invalid_payload: - status.invalid_payload.add(True) - if blacklisted: - status.blacklisted.add(True) - if suspected: - status.suspected.add(True) - - status.num_unknown_offenses = num_unknown_offenses - status.num_double_signed = num_double_signed - status.num_light_client_attack = num_light_client_attack - status.custom_offences_amount = custom_amount - - actual = status.slash_amount(light_unit_amount, serious_unit_amount) - assert actual == expected + status.custom_offences_amount - - -@composite -def offence_tracking(draw: DrawFn) -> Tuple[Evidences, LastCommitInfo]: - """A strategy for building offences reported by Tendermint.""" - n_validators = draw(integers(min_value=1, max_value=10)) - - validators = [ - draw( - builds( - Validator, - address=just(bytes(i)), - power=integers(min_value=0), - ) - ) - for i in range(n_validators) - ] - - evidences = builds( - Evidences, - byzantine_validators=lists( - builds( - Evidence, - evidence_type=sampled_from(EvidenceType), - validator=sampled_from(validators), - height=integers(min_value=0), - time=builds( - Timestamp, - seconds=integers(min_value=0), - nanos=integers(min_value=0, max_value=999_999_999), - ), - total_voting_power=integers(min_value=0), - ), - min_size=n_validators, - max_size=n_validators, - unique_by=lambda v: v.validator.address, - ), - ) - - last_commit_info = builds( - LastCommitInfo, - round_=integers(min_value=0), - votes=lists( - builds( - VoteInfo, - validator=sampled_from(validators), - signed_last_block=booleans(), - ), - min_size=n_validators, - max_size=n_validators, - unique_by=lambda v: v.validator.address, - ), - ) - - ev_example, commit_example = draw(evidences), draw(last_commit_info) - - # this assertion proves that all the validators are unique - unique_commit_addresses = set( - v.validator.address.decode() for v in commit_example.votes - ) - assert len(unique_commit_addresses) == n_validators - - # this assertion proves that the same validators are used for evidences and votes - assert unique_commit_addresses == set( - e.validator.address.decode() for e in ev_example.byzantine_validators - ) - - return ev_example, commit_example - - -@composite -def offence_status(draw: DrawFn) -> OffenceStatus: - """Build an offence status instance.""" - validator_downtime = just( - AvailabilityWindow.from_dict(draw(availability_window_data())) - ) - invalid_payload = just( - AvailabilityWindow.from_dict(draw(availability_window_data())) - ) - blacklisted = just(AvailabilityWindow.from_dict(draw(availability_window_data()))) - suspected = just(AvailabilityWindow.from_dict(draw(availability_window_data()))) - - status = builds( - OffenceStatus, - validator_downtime=validator_downtime, - invalid_payload=invalid_payload, - blacklisted=blacklisted, - suspected=suspected, - num_unknown_offenses=integers(min_value=0), - num_double_signed=integers(min_value=0), - num_light_client_attack=integers(min_value=0), - ) - - return draw(status) - - -class TestOffenseStatusEncoderDecoder: - """Test the `OffenseStatusEncoder` and the `OffenseStatusDecoder`.""" - - @staticmethod - @given(dictionaries(keys=text(), values=offence_status(), min_size=1)) - def test_encode_decode_offense_status(offense_status: str) -> None: - """Test encoding an offense status mapping and then decoding it by using the custom encoder/decoder.""" - encoded = json.dumps(offense_status, cls=OffenseStatusEncoder) - decoded = json.loads(encoded, cls=OffenseStatusDecoder) - - assert decoded == offense_status - - def test_encode_unknown(self) -> None: - """Test the encoder with an unknown input.""" - - class Unknown: - """A dummy class that the encoder is not aware of.""" - - unknown = "?" - - with pytest.raises( - TypeError, match="Object of type Unknown is not JSON serializable" - ): - json.dumps(Unknown(), cls=OffenseStatusEncoder) - - -class TestRoundSequence: - """Test the RoundSequence class.""" - - def setup(self) -> None: - """Set up the test.""" - self.round_sequence = RoundSequence( - context=MagicMock(), abci_app_cls=AbciAppTest - ) - self.round_sequence.setup(MagicMock(), logging.getLogger()) - self.round_sequence.tm_height = 1 - - @pytest.mark.parametrize( - "property_name, set_twice_exc, config_exc", - ( - ( - "validator_to_agent", - "The mapping of the validators' addresses to their agent addresses can only be set once. " - "Attempted to set with {new_content_attempt} but it has content already: {value}.", - "The mapping of the validators' addresses to their agent addresses has not been set.", - ), - ), - ) - @given(data()) - def test_slashing_properties( - self, property_name: str, set_twice_exc: str, config_exc: str, _data: Any - ) -> None: - """Test `validator_to_agent` getter and setter.""" - if property_name == "validator_to_agent": - data_generator = dictionaries(text(), text()) - else: - data_generator = dictionaries(text(), just(OffenceStatus())) - - value = _data.draw(data_generator) - round_sequence = RoundSequence(context=MagicMock(), abci_app_cls=AbciAppTest) - - if value: - setattr(round_sequence, property_name, value) - assert getattr(round_sequence, property_name) == value - new_content_attempt = _data.draw(data_generator) - with pytest.raises( - ValueError, - match=re.escape( - set_twice_exc.format( - new_content_attempt=new_content_attempt, value=value - ) - ), - ): - setattr(round_sequence, property_name, new_content_attempt) - return - - with pytest.raises(SlashingNotConfiguredError, match=config_exc): - getattr(round_sequence, property_name) - - @mock.patch("json.loads", return_value="json_serializable") - @pytest.mark.parametrize("slashing_config", (None, "", "test")) - def test_sync_db_and_slashing( - self, mock_loads: mock.MagicMock, slashing_config: str - ) -> None: - """Test the `sync_db_and_slashing` method.""" - self.round_sequence.latest_synchronized_data.slashing_config = slashing_config - serialized_db_state = "dummy_db_state" - self.round_sequence.sync_db_and_slashing(serialized_db_state) - - # Check that `sync()` was called with the correct arguments - mock_sync = cast( - mock.Mock, self.round_sequence.abci_app.synchronized_data.db.sync - ) - mock_sync.assert_called_once_with(serialized_db_state) - - if slashing_config: - mock_loads.assert_called_once_with( - slashing_config, cls=OffenseStatusDecoder - ) - else: - mock_loads.assert_not_called() - - @mock.patch("json.dumps") - @pytest.mark.parametrize("slashing_enabled", (True, False)) - def test_store_offence_status( - self, mock_dumps: mock.MagicMock, slashing_enabled: bool - ) -> None: - """Test the `store_offence_status` method.""" - # Set up mock objects and return values - self.round_sequence._offence_status = {"not_encoded": OffenceStatus()} - mock_encoded_status = "encoded_status" - mock_dumps.return_value = mock_encoded_status - - self.round_sequence._slashing_enabled = slashing_enabled - - # Call the method to be tested - self.round_sequence.store_offence_status() - - if slashing_enabled: - # Check that `json.dumps()` was called with the correct arguments, only if slashing is enabled - mock_dumps.assert_called_once_with( - self.round_sequence.offence_status, - cls=OffenseStatusEncoder, - sort_keys=True, - ) - assert ( - self.round_sequence.abci_app.synchronized_data.db.slashing_config - == mock_encoded_status - ) - return - - # otherwise check that it was not called - mock_dumps.assert_not_called() - - @given( - validator=builds(Validator, address=binary(), power=integers()), - agent_address=text(), - ) - def test_get_agent_address(self, validator: Validator, agent_address: str) -> None: - """Test `get_agent_address` method.""" - round_sequence = RoundSequence(context=MagicMock(), abci_app_cls=AbciAppTest) - round_sequence.validator_to_agent = { - validator.address.hex().upper(): agent_address - } - assert round_sequence.get_agent_address(validator) == agent_address - - unknown = deepcopy(validator) - unknown.address += b"unknown" - with pytest.raises( - ValueError, - match=re.escape( - f"Requested agent address for an unknown validator address {unknown.address.hex().upper()}. " - f"Available validators are: {round_sequence.validator_to_agent.keys()}" - ), - ): - round_sequence.get_agent_address(unknown) - - @pytest.mark.parametrize("offset", tuple(range(5))) - @pytest.mark.parametrize("n_blocks", (0, 1, 10)) - def test_height(self, n_blocks: int, offset: int) -> None: - """Test 'height' property.""" - self.round_sequence._blockchain._blocks = [MagicMock() for _ in range(n_blocks)] - self.round_sequence._blockchain._height_offset = offset - assert self.round_sequence._blockchain.length == n_blocks - assert self.round_sequence.height == n_blocks + offset - - def test_is_finished(self) -> None: - """Test 'is_finished' property.""" - assert not self.round_sequence.is_finished - self.round_sequence.abci_app._current_round = None - assert self.round_sequence.is_finished - - def test_last_round(self) -> None: - """Test 'last_round' property.""" - assert self.round_sequence.last_round_id is None - - def test_last_timestamp_none(self) -> None: - """ - Test 'last_timestamp' property. - - The property is None because there are no blocks. - """ - with pytest.raises(ABCIAppInternalError, match="last timestamp is None"): - self.round_sequence.last_timestamp - - def test_last_timestamp(self) -> None: - """Test 'last_timestamp' property, positive case.""" - seconds = 1 - nanoseconds = 1000 - expected_timestamp = datetime.datetime.fromtimestamp( - seconds + nanoseconds / 10**9 - ) - self.round_sequence._blockchain.add_block( - Block(MagicMock(height=1, timestamp=expected_timestamp), []) - ) - assert self.round_sequence.last_timestamp == expected_timestamp - - def test_abci_app_negative(self) -> None: - """Test 'abci_app' property, negative case.""" - self.round_sequence._abci_app = None - with pytest.raises(ABCIAppInternalError, match="AbciApp not set"): - self.round_sequence.abci_app - - def test_check_is_finished_negative(self) -> None: - """Test 'check_is_finished', negative case.""" - self.round_sequence.abci_app._current_round = None - with pytest.raises( - ValueError, - match="round sequence is finished, cannot accept new transactions", - ): - self.round_sequence.check_is_finished() - - def test_current_round_positive(self) -> None: - """Test 'current_round' property getter, positive case.""" - assert isinstance(self.round_sequence.current_round, ConcreteRoundA) - - def test_current_round_negative_current_round_not_set(self) -> None: - """Test 'current_round' property getter, negative case (current round not set).""" - self.round_sequence.abci_app._current_round = None - with pytest.raises(ValueError, match="current_round not set!"): - self.round_sequence.current_round - - def test_current_round_id(self) -> None: - """Test 'current_round_id' property getter""" - assert self.round_sequence.current_round_id == ConcreteRoundA.auto_round_id() - - def test_latest_result(self) -> None: - """Test 'latest_result' property getter.""" - assert self.round_sequence.latest_synchronized_data - - @pytest.mark.parametrize("committed", (True, False)) - def test_last_round_transition_timestamp(self, committed: bool) -> None: - """Test 'last_round_transition_timestamp' method.""" - if committed: - self.round_sequence.begin_block( - MagicMock(height=1), MagicMock(), MagicMock() - ) - self.round_sequence.end_block() - self.round_sequence.commit() - assert ( - self.round_sequence.last_round_transition_timestamp - == self.round_sequence._blockchain.last_block.timestamp - ) - else: - assert self.round_sequence._blockchain.height == 0 - with pytest.raises( - ValueError, - match="Trying to access `last_round_transition_timestamp` while no transition has been completed yet.", - ): - _ = self.round_sequence.last_round_transition_timestamp - - @pytest.mark.parametrize("committed", (True, False)) - def test_last_round_transition_height(self, committed: bool) -> None: - """Test 'last_round_transition_height' method.""" - if committed: - self.round_sequence.begin_block( - MagicMock(height=1), MagicMock(), MagicMock() - ) - self.round_sequence.end_block() - self.round_sequence.commit() - assert ( - self.round_sequence.last_round_transition_height - == self.round_sequence._blockchain.height - == 1 - ) - else: - assert self.round_sequence._blockchain.height == 0 - with pytest.raises( - ValueError, - match="Trying to access `last_round_transition_height` while no transition has been completed yet.", - ): - _ = self.round_sequence.last_round_transition_height - - def test_block_before_blockchain_is_init(self, caplog: LogCaptureFixture) -> None: - """Test block received before blockchain initialized.""" - - self.round_sequence.begin_block(MagicMock(height=1), MagicMock(), MagicMock()) - self.round_sequence.end_block() - blockchain = self.round_sequence.blockchain - blockchain._is_init = False - self.round_sequence.blockchain = blockchain - with caplog.at_level(logging.INFO): - self.round_sequence.commit() - expected = "Received block with height 1 before the blockchain was initialized." - assert expected in caplog.text - - @pytest.mark.parametrize("last_round_transition_root_hash", (b"", b"test")) - def test_last_round_transition_root_hash( - self, - last_round_transition_root_hash: bytes, - ) -> None: - """Test 'last_round_transition_root_hash' method.""" - self.round_sequence._last_round_transition_root_hash = ( - last_round_transition_root_hash - ) - - if last_round_transition_root_hash == b"": - with mock.patch.object( - RoundSequence, - "root_hash", - new_callable=mock.PropertyMock, - return_value="test", - ): - assert self.round_sequence.last_round_transition_root_hash == "test" - else: - assert ( - self.round_sequence.last_round_transition_root_hash - == last_round_transition_root_hash - ) - - @pytest.mark.parametrize("tm_height", (None, 1, 5)) - def test_last_round_transition_tm_height(self, tm_height: Optional[int]) -> None: - """Test 'last_round_transition_tm_height' method.""" - if tm_height is None: - with pytest.raises( - ValueError, - match="Trying to access Tendermint's last round transition height before any `end_block` calls.", - ): - _ = self.round_sequence.last_round_transition_tm_height - else: - self.round_sequence.tm_height = tm_height - self.round_sequence.begin_block( - MagicMock(height=1), MagicMock(), MagicMock() - ) - self.round_sequence.end_block() - self.round_sequence.commit() - assert self.round_sequence.last_round_transition_tm_height == tm_height - - @given(one_of(none(), integers())) - def test_tm_height(self, tm_height: int) -> None: - """Test `tm_height` getter and setter.""" - - self.round_sequence.tm_height = tm_height - - if tm_height is None: - with pytest.raises( - ValueError, - match="Trying to access Tendermint's current height before any `end_block` calls.", - ): - _ = self.round_sequence.tm_height - else: - assert ( - self.round_sequence.tm_height - == self.round_sequence._tm_height - == tm_height - ) - - @given(one_of(none(), datetimes())) - def test_block_stall_deadline_expired( - self, block_stall_deadline: datetime.datetime - ) -> None: - """Test 'block_stall_deadline_expired' method.""" - - self.round_sequence._block_stall_deadline = block_stall_deadline - actual = self.round_sequence.block_stall_deadline_expired - - if block_stall_deadline is None: - assert actual is False - else: - expected = datetime.datetime.now() > block_stall_deadline - assert actual is expected - - @pytest.mark.parametrize("begin_height", tuple(range(0, 50, 10))) - @pytest.mark.parametrize("initial_height", tuple(range(0, 11, 5))) - def test_init_chain(self, begin_height: int, initial_height: int) -> None: - """Test 'init_chain' method.""" - for i in range(begin_height): - self.round_sequence._blockchain.add_block( - MagicMock(header=MagicMock(height=i + 1)) - ) - assert self.round_sequence._blockchain.height == begin_height - self.round_sequence.init_chain(initial_height) - assert self.round_sequence._blockchain.height == initial_height - 1 - - @given(offence_tracking()) - @settings(suppress_health_check=[HealthCheck.too_slow]) - def test_track_tm_offences( - self, offences: Tuple[Evidences, LastCommitInfo] - ) -> None: - """Test `_track_tm_offences` method.""" - evidences, last_commit_info = offences - dummy_addr_template = "agent_{i}" - round_sequence = RoundSequence(context=MagicMock(), abci_app_cls=AbciAppTest) - synchronized_data_mock = MagicMock() - round_sequence.setup(synchronized_data_mock, MagicMock()) - round_sequence.enable_slashing() - - expected_offence_status = { - dummy_addr_template.format(i=i): OffenceStatus() - for i in range(len(last_commit_info.votes)) - } - for i, vote_info in enumerate(last_commit_info.votes): - agent_address = dummy_addr_template.format(i=i) - # initialize dummy round sequence's offence status and validator to agent address mapping - round_sequence._offence_status[agent_address] = OffenceStatus() - validator_address = vote_info.validator.address.hex() - round_sequence._validator_to_agent[validator_address] = agent_address - # set expected result - expected_was_down = not vote_info.signed_last_block - expected_offence_status[agent_address].validator_downtime.add( - expected_was_down - ) - - for byzantine_validator in evidences.byzantine_validators: - agent_address = round_sequence._validator_to_agent[ - byzantine_validator.validator.address.hex() - ] - evidence_type = byzantine_validator.evidence_type - expected_offence_status[agent_address].num_unknown_offenses += bool( - evidence_type == EvidenceType.UNKNOWN - ) - expected_offence_status[agent_address].num_double_signed += bool( - evidence_type == EvidenceType.DUPLICATE_VOTE - ) - expected_offence_status[agent_address].num_light_client_attack += bool( - evidence_type == EvidenceType.LIGHT_CLIENT_ATTACK - ) - - round_sequence._track_tm_offences(evidences, last_commit_info) - assert round_sequence._offence_status == expected_offence_status - - @mock.patch.object(abci_base, "ADDRESS_LENGTH", len("agent_i")) - def test_track_app_offences(self) -> None: - """Test `_track_app_offences` method.""" - dummy_addr_template = "agent_{i}" - stub_offending_keepers = [dummy_addr_template.format(i=i) for i in range(2)] - self.round_sequence.enable_slashing() - self.round_sequence._offence_status = { - dummy_addr_template.format(i=i): OffenceStatus() for i in range(4) - } - expected_offence_status = deepcopy(self.round_sequence._offence_status) - - for i in (dummy_addr_template.format(i=i) for i in range(4)): - offended = i in stub_offending_keepers - expected_offence_status[i].blacklisted.add(offended) - expected_offence_status[i].suspected.add(offended) - - with mock.patch.object( - self.round_sequence.latest_synchronized_data.db, - "get", - return_value="".join(stub_offending_keepers), - ): - self.round_sequence._track_app_offences() - assert self.round_sequence._offence_status == expected_offence_status - - @given(builds(SlashingNotConfiguredError, text())) - def test_handle_slashing_not_configured( - self, exc: SlashingNotConfiguredError - ) -> None: - """Test `_handle_slashing_not_configured` method.""" - logging.disable(logging.CRITICAL) - - round_sequence = RoundSequence(context=MagicMock(), abci_app_cls=AbciAppTest) - round_sequence.setup(MagicMock(), MagicMock()) - - assert not round_sequence._slashing_enabled - assert round_sequence.latest_synchronized_data.nb_participants == 0 - round_sequence._handle_slashing_not_configured(exc) - assert not round_sequence._slashing_enabled - - with mock.patch.object( - round_sequence.latest_synchronized_data.db, - "get", - return_value=[i for i in range(4)], - ): - assert round_sequence.latest_synchronized_data.nb_participants == 4 - round_sequence._handle_slashing_not_configured(exc) - assert not round_sequence._slashing_enabled - - logging.disable(logging.NOTSET) - - @pytest.mark.parametrize("_track_offences_raises", (True, False)) - def test_try_track_offences(self, _track_offences_raises: bool) -> None: - """Test `_try_track_offences` method.""" - evidences, last_commit_info = MagicMock(), MagicMock() - self.round_sequence.enable_slashing() - with mock.patch.object( - self.round_sequence, - "_track_app_offences", - ), mock.patch.object( - self.round_sequence, - "_track_tm_offences", - side_effect=SlashingNotConfiguredError if _track_offences_raises else None, - ) as _track_offences_mock, mock.patch.object( - self.round_sequence, "_handle_slashing_not_configured" - ) as _handle_slashing_not_configured_mock: - self.round_sequence._try_track_offences(evidences, last_commit_info) - if _track_offences_raises: - _handle_slashing_not_configured_mock.assert_called_once() - else: - _track_offences_mock.assert_called_once_with( - evidences, last_commit_info - ) - - def test_begin_block_negative_is_finished(self) -> None: - """Test 'begin_block' method, negative case (round sequence is finished).""" - self.round_sequence.abci_app._current_round = None - with pytest.raises( - ABCIAppInternalError, - match="internal error: round sequence is finished, cannot accept new blocks", - ): - self.round_sequence.begin_block(MagicMock(), MagicMock(), MagicMock()) - - def test_begin_block_negative_wrong_phase(self) -> None: - """Test 'begin_block' method, negative case (wrong phase).""" - self.round_sequence._block_construction_phase = MagicMock() - with pytest.raises( - ABCIAppInternalError, - match="internal error: cannot accept a 'begin_block' request.", - ): - self.round_sequence.begin_block(MagicMock(), MagicMock(), MagicMock()) - - def test_begin_block_positive(self) -> None: - """Test 'begin_block' method, positive case.""" - self.round_sequence.begin_block(MagicMock(), MagicMock(), MagicMock()) - - def test_deliver_tx_negative_wrong_phase(self) -> None: - """Test 'begin_block' method, negative (wrong phase).""" - with pytest.raises( - ABCIAppInternalError, - match="internal error: cannot accept a 'deliver_tx' request", - ): - self.round_sequence.deliver_tx(MagicMock()) - - def test_deliver_tx_positive_not_valid(self) -> None: - """Test 'begin_block' method, positive (not valid).""" - self.round_sequence.begin_block(MagicMock(), MagicMock(), MagicMock()) - with mock.patch.object( - self.round_sequence.current_round, "check_transaction", return_value=True - ): - with mock.patch.object( - self.round_sequence.current_round, "process_transaction" - ): - self.round_sequence.deliver_tx(MagicMock()) - - def test_end_block_negative_wrong_phase(self) -> None: - """Test 'end_block' method, negative case (wrong phase).""" - with pytest.raises( - ABCIAppInternalError, - match="internal error: cannot accept a 'end_block' request.", - ): - self.round_sequence.end_block() - - def test_end_block_positive(self) -> None: - """Test 'end_block' method, positive case.""" - self.round_sequence.begin_block(MagicMock(), MagicMock(), MagicMock()) - self.round_sequence.end_block() - - def test_commit_negative_wrong_phase(self) -> None: - """Test 'end_block' method, negative case (wrong phase).""" - with pytest.raises( - ABCIAppInternalError, - match="internal error: cannot accept a 'commit' request.", - ): - self.round_sequence.commit() - - def test_commit_negative_exception(self) -> None: - """Test 'end_block' method, negative case (raise exception).""" - self.round_sequence.begin_block(MagicMock(height=1), MagicMock(), MagicMock()) - self.round_sequence.end_block() - with mock.patch.object( - self.round_sequence._blockchain, "add_block", side_effect=AddBlockError - ): - with pytest.raises(AddBlockError): - self.round_sequence.commit() - - def test_commit_positive_no_change_round(self) -> None: - """Test 'end_block' method, positive (no change round).""" - self.round_sequence.begin_block(MagicMock(height=1), MagicMock(), MagicMock()) - self.round_sequence.end_block() - with mock.patch.object( - self.round_sequence.current_round, - "end_block", - return_value=None, - ): - assert isinstance(self.round_sequence.current_round, ConcreteRoundA) - - def test_commit_positive_with_change_round(self) -> None: - """Test 'end_block' method, positive (with change round).""" - self.round_sequence.begin_block(MagicMock(height=1), MagicMock(), MagicMock()) - self.round_sequence.end_block() - round_result, next_round = MagicMock(), MagicMock() - with mock.patch.object( - self.round_sequence.current_round, - "end_block", - return_value=(round_result, next_round), - ): - self.round_sequence.commit() - assert not isinstance( - self.round_sequence.abci_app._current_round, ConcreteRoundA - ) - assert self.round_sequence.latest_synchronized_data == round_result - - @pytest.mark.parametrize("is_replay", (True, False)) - def test_reset_blockchain(self, is_replay: bool) -> None: - """Test `reset_blockchain` method.""" - self.round_sequence.reset_blockchain(is_replay) - if is_replay: - assert ( - self.round_sequence._block_construction_phase - == RoundSequence._BlockConstructionState.WAITING_FOR_BEGIN_BLOCK - ) - assert self.round_sequence._blockchain.height == 0 - - def last_round_values_updated(self, any_: bool = True) -> bool: - """Check if the values for the last round-related attributes have been updated.""" - seq = self.round_sequence - - current_last_pairs = ( - ( - seq._blockchain.last_block.timestamp, - seq._last_round_transition_timestamp, - ), - (seq._blockchain.height, seq._last_round_transition_height), - (seq.root_hash, seq._last_round_transition_root_hash), - (seq.tm_height, seq._last_round_transition_tm_height), - ) - - if any_: - return any(current == last for current, last in current_last_pairs) - - return all(current == last for current, last in current_last_pairs) - - @mock.patch.object(AbciApp, "process_event") - @mock.patch.object(RoundSequence, "serialized_offence_status") - @pytest.mark.parametrize("end_block_res", (None, (MagicMock(), MagicMock()))) - @pytest.mark.parametrize( - "slashing_enabled, offence_status_", - ( - ( - False, - False, - ), - ( - False, - True, - ), - ( - False, - False, - ), - ( - True, - True, - ), - ), - ) - def test_update_round( - self, - serialized_offence_status_mock: mock.Mock, - process_event_mock: mock.Mock, - end_block_res: Optional[Tuple[BaseSynchronizedData, Any]], - slashing_enabled: bool, - offence_status_: dict, - ) -> None: - """Test '_update_round' method.""" - self.round_sequence.begin_block(MagicMock(height=1), MagicMock(), MagicMock()) - block = self.round_sequence._block_builder.get_block() - self.round_sequence._blockchain.add_block(block) - self.round_sequence._slashing_enabled = slashing_enabled - self.round_sequence._offence_status = offence_status_ - - with mock.patch.object( - self.round_sequence.current_round, "end_block", return_value=end_block_res - ): - self.round_sequence._update_round() - - if end_block_res is None: - assert not self.last_round_values_updated() - process_event_mock.assert_not_called() - return - - assert self.last_round_values_updated(any_=False) - process_event_mock.assert_called_with( - end_block_res[-1], result=end_block_res[0] - ) - - if slashing_enabled: - serialized_offence_status_mock.assert_called_once() - else: - serialized_offence_status_mock.assert_not_called() - - @mock.patch.object(AbciApp, "process_event") - @pytest.mark.parametrize( - "termination_round_result, current_round_result", - [ - (None, None), - (None, (MagicMock(), MagicMock())), - ((MagicMock(), MagicMock()), None), - ((MagicMock(), MagicMock()), (MagicMock(), MagicMock())), - ], - ) - def test_update_round_when_termination_returns( - self, - process_event_mock: mock.Mock, - termination_round_result: Optional[Tuple[BaseSynchronizedData, Any]], - current_round_result: Optional[Tuple[BaseSynchronizedData, Any]], - ) -> None: - """Test '_update_round' method.""" - self.round_sequence.begin_block(MagicMock(height=1), MagicMock(), MagicMock()) - block = self.round_sequence._block_builder.get_block() - self.round_sequence._blockchain.add_block(block) - self.round_sequence.abci_app.add_background_app(STUB_TERMINATION_CONFIG) - self.round_sequence.abci_app.setup() - - with mock.patch.object( - self.round_sequence.current_round, - "end_block", - return_value=current_round_result, - ), mock.patch.object( - ConcreteBackgroundRound, - "end_block", - return_value=termination_round_result, - ): - self.round_sequence._update_round() - - if termination_round_result is None and current_round_result is None: - assert ( - self.round_sequence._last_round_transition_timestamp - != self.round_sequence._blockchain.last_block.timestamp - ) - assert ( - self.round_sequence._last_round_transition_height - != self.round_sequence._blockchain.height - ) - assert ( - self.round_sequence._last_round_transition_root_hash - != self.round_sequence.root_hash - ) - assert ( - self.round_sequence._last_round_transition_tm_height - != self.round_sequence.tm_height - ) - process_event_mock.assert_not_called() - elif termination_round_result is None and current_round_result is not None: - assert ( - self.round_sequence._last_round_transition_timestamp - == self.round_sequence._blockchain.last_block.timestamp - ) - assert ( - self.round_sequence._last_round_transition_height - == self.round_sequence._blockchain.height - ) - assert ( - self.round_sequence._last_round_transition_root_hash - == self.round_sequence.root_hash - ) - assert ( - self.round_sequence._last_round_transition_tm_height - == self.round_sequence.tm_height - ) - process_event_mock.assert_called_with( - current_round_result[-1], - result=current_round_result[0], - ) - elif termination_round_result is not None: - assert ( - self.round_sequence._last_round_transition_timestamp - == self.round_sequence._blockchain.last_block.timestamp - ) - assert ( - self.round_sequence._last_round_transition_height - == self.round_sequence._blockchain.height - ) - assert ( - self.round_sequence._last_round_transition_root_hash - == self.round_sequence.root_hash - ) - assert ( - self.round_sequence._last_round_transition_tm_height - == self.round_sequence.tm_height - ) - process_event_mock.assert_called_with( - termination_round_result[-1], - result=termination_round_result[0], - ) - - self.round_sequence.abci_app.background_apps.clear() - - @pytest.mark.parametrize("restart_from_round", (ConcreteRoundA, MagicMock())) - @pytest.mark.parametrize("serialized_db_state", (None, "serialized state")) - @given(integers()) - def test_reset_state( - self, - restart_from_round: AbstractRound, - serialized_db_state: str, - round_count: int, - ) -> None: - """Tests reset_state""" - with mock.patch.object( - self.round_sequence, - "_reset_to_default_params", - ) as mock_reset, mock.patch.object( - self.round_sequence, "sync_db_and_slashing" - ) as mock_sync_db_and_slashing: - transition_fn = self.round_sequence.abci_app.transition_function - round_id = restart_from_round.auto_round_id() - if restart_from_round in transition_fn: - self.round_sequence.reset_state( - round_id, round_count, serialized_db_state - ) - mock_reset.assert_called() - - if serialized_db_state is None: - mock_sync_db_and_slashing.assert_not_called() - - else: - mock_sync_db_and_slashing.assert_called_once_with( - serialized_db_state - ) - assert ( - self.round_sequence._last_round_transition_root_hash - == self.round_sequence.root_hash - ) - - else: - round_ids = {cls.auto_round_id() for cls in transition_fn} - with pytest.raises( - ABCIAppInternalError, - match=re.escape( - "internal error: Cannot reset state. The Tendermint recovery parameters are incorrect. " - "Did you update the `restart_from_round` with an incorrect round id? " - f"Found {round_id}, but the app's transition function has the following round ids: " - f"{round_ids}.", - ), - ): - self.round_sequence.reset_state( - restart_from_round.auto_round_id(), - round_count, - serialized_db_state, - ) - - def test_reset_to_default_params(self) -> None: - """Tests _reset_to_default_params.""" - # we set some values to the parameters, to make sure that they are not "empty" - self.round_sequence._last_round_transition_timestamp = MagicMock() - self.round_sequence._last_round_transition_height = MagicMock() - self.round_sequence._last_round_transition_root_hash = MagicMock() - self.round_sequence._last_round_transition_tm_height = MagicMock() - self.round_sequence._tm_height = MagicMock() - self._pending_offences = MagicMock() - self._slashing_enabled = MagicMock() - - # we reset them - self.round_sequence._reset_to_default_params() - - # we check whether they have been reset - assert self.round_sequence._last_round_transition_timestamp is None - assert self.round_sequence._last_round_transition_height == 0 - assert self.round_sequence._last_round_transition_root_hash == b"" - assert self.round_sequence._last_round_transition_tm_height is None - assert self.round_sequence._tm_height is None - assert self.round_sequence.pending_offences == set() - assert not self.round_sequence._slashing_enabled - - def test_add_pending_offence(self) -> None: - """Tests add_pending_offence.""" - assert self.round_sequence.pending_offences == set() - mock_offence = MagicMock() - self.round_sequence.add_pending_offence(mock_offence) - assert self.round_sequence.pending_offences == {mock_offence} - - -def test_meta_abci_app_when_instance_not_subclass_of_abstract_round() -> None: - """ - Test instantiation of meta-class when instance not a subclass of AbciApp. - - Since the class is not a subclass of AbciApp, the checks performed by - the meta-class should not apply. - """ - - class MyAbciApp(metaclass=_MetaAbciApp): - pass - - -def test_meta_abci_app_when_final_round_not_subclass_of_degenerate_round() -> None: - """Test instantiation of meta-class when a final round is not a subclass of DegenerateRound.""" - - class FinalRound(AbstractRound, ABC): - """A round class for testing.""" - - payload_class = MagicMock() - synchronized_data_class = MagicMock() - payload_attribute = MagicMock() - round_id = "final_round" - - with pytest.raises( - AEAEnforceError, - match="non-final state.*must have at least one non-timeout transition", - ): - - class MyAbciApp(AbciApp, metaclass=_MetaAbciApp): - initial_round_cls: Type[AbstractRound] = ConcreteRoundA - transition_function: Dict[ - Type[AbstractRound], Dict[str, Type[AbstractRound]] - ] = { - ConcreteRoundA: {"event": FinalRound, "timeout": ConcreteRoundA}, - FinalRound: {}, - } - event_to_timeout = {"timeout": 1.0} - final_states: Set[AppState] = set() - - -def test_synchronized_data_type_on_abci_app_init(caplog: LogCaptureFixture) -> None: - """Test synchronized data access""" - - # NOTE: the synchronized data of a particular AbciApp is only - # updated at the end of a round. However, we want to make sure - # that the instance during the first round of any AbciApp is - # in fact and instance of the locally defined SynchronizedData - - sentinel = object() - - class SynchronizedData(BaseSynchronizedData): - """SynchronizedData""" - - @property - def dummy_attr(self) -> object: - return sentinel - - # this is how it's setup in SharedState.setup, using BaseSynchronizedData - synchronized_data = BaseSynchronizedData(db=AbciAppDB(setup_data={})) - - with mock.patch.object(AbciAppTest, "initial_round_cls") as m: - m.synchronized_data_class = SynchronizedData - abci_app = AbciAppTest(synchronized_data, logging.getLogger(), MagicMock()) - abci_app.setup() - assert isinstance(abci_app.synchronized_data, SynchronizedData) - assert abci_app.synchronized_data.dummy_attr == sentinel - - -def test_get_name() -> None: - """Test the get_name method.""" - - class SomeObject: - @property - def some_property(self) -> Any: - """Some getter.""" - return object() - - assert get_name(SomeObject.some_property) == "some_property" - with pytest.raises(ValueError, match="1 is not a property"): - get_name(1) - - -@pytest.mark.parametrize( - "sender, accused_agent_address, offense_round, offense_type_value, last_transition_timestamp, time_to_live, custom_amount", - ( - ( - "sender", - "test_address", - 90, - 3, - 10, - 2, - 10, - ), - ), -) -def test_pending_offences_payload( - sender: str, - accused_agent_address: str, - offense_round: int, - offense_type_value: int, - last_transition_timestamp: int, - time_to_live: int, - custom_amount: int, -) -> None: - """Test `PendingOffencesPayload`""" - - payload = abci_base.PendingOffencesPayload( - sender, - accused_agent_address, - offense_round, - offense_type_value, - last_transition_timestamp, - time_to_live, - custom_amount, - ) - - assert payload.id_ - assert payload.round_count == abci_base.ROUND_COUNT_DEFAULT - assert payload.sender == sender - assert payload.accused_agent_address == accused_agent_address - assert payload.offense_round == offense_round - assert payload.offense_type_value == offense_type_value - assert payload.last_transition_timestamp == last_transition_timestamp - assert payload.time_to_live == time_to_live - assert payload.custom_amount == custom_amount - assert payload.data == { - "accused_agent_address": accused_agent_address, - "offense_round": offense_round, - "offense_type_value": offense_type_value, - "last_transition_timestamp": last_transition_timestamp, - "time_to_live": time_to_live, - "custom_amount": custom_amount, - } - - -class TestPendingOffencesRound(BaseRoundTestClass): - """Tests for `PendingOffencesRound`.""" - - _synchronized_data_class = BaseSynchronizedData - - @given( - accused_agent_address=sampled_from(list(get_participants())), - offense_round=integers(min_value=0), - offense_type_value=sampled_from( - [value.value for value in OffenseType.__members__.values()] - ), - last_transition_timestamp=floats( - min_value=timegm(datetime.datetime(1971, 1, 1).utctimetuple()), - max_value=timegm(datetime.datetime(8000, 1, 1).utctimetuple()) - 2000, - ), - time_to_live=floats(min_value=1, max_value=2000), - custom_amount=integers(min_value=0), - ) - def test_run( - self, - accused_agent_address: str, - offense_round: int, - offense_type_value: int, - last_transition_timestamp: float, - time_to_live: float, - custom_amount: int, - ) -> None: - """Run tests.""" - - test_round = abci_base.PendingOffencesRound( - synchronized_data=self.synchronized_data, - context=MagicMock(), - ) - # initialize the offence status - status_initialization = dict.fromkeys(self.participants, OffenceStatus()) - test_round.context.state.round_sequence.offence_status = status_initialization - - # create the actual and expected value - actual = test_round.context.state.round_sequence.offence_status - expected_invalid = offense_type_value == OffenseType.INVALID_PAYLOAD.value - expected_custom_amount = offense_type_value == OffenseType.CUSTOM.value - expected = deepcopy(status_initialization) - - first_payload, *payloads = [ - abci_base.PendingOffencesPayload( - sender, - accused_agent_address, - offense_round, - offense_type_value, - last_transition_timestamp, - time_to_live, - custom_amount, - ) - for sender in self.participants - ] - - test_round.process_payload(first_payload) - assert test_round.collection == {first_payload.sender: first_payload} - test_round.end_block() - assert actual == expected - - for payload in payloads: - test_round.process_payload(payload) - test_round.end_block() - - expected[accused_agent_address].invalid_payload.add(expected_invalid) - if expected_custom_amount: - expected[accused_agent_address].custom_offences_amount += custom_amount - - assert actual == expected diff --git a/packages/valory/skills/abstract_round_abci/tests/test_base_rounds.py b/packages/valory/skills/abstract_round_abci/tests/test_base_rounds.py deleted file mode 100644 index 06da258..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/test_base_rounds.py +++ /dev/null @@ -1,668 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test the base round classes.""" - -# pylint: skip-file - -import re -from enum import Enum -from typing import FrozenSet, List, Optional, Tuple, Union, cast -from unittest.mock import MagicMock - -import pytest - -from packages.valory.skills.abstract_round_abci.base import ( - ABCIAppInternalError, - BaseSynchronizedData, - BaseTxPayload, - TransactionNotValidError, -) -from packages.valory.skills.abstract_round_abci.test_tools.rounds import ( - BaseOnlyKeeperSendsRoundTest, - DummyCollectDifferentUntilAllRound, - DummyCollectDifferentUntilThresholdRound, - DummyCollectNonEmptyUntilThresholdRound, - DummyCollectSameUntilAllRound, - DummyCollectSameUntilThresholdRound, - DummyCollectionRound, - DummyEvent, - DummyOnlyKeeperSendsRound, - DummyTxPayload, - DummyVotingRound, - MAX_PARTICIPANTS, - _BaseRoundTestClass, - get_dummy_tx_payloads, -) - - -class TestCollectionRound(_BaseRoundTestClass): - """Test class for CollectionRound.""" - - def setup( - self, - ) -> None: - """Setup test.""" - super().setup() - - self.test_round = DummyCollectionRound( - synchronized_data=self.synchronized_data, context=MagicMock() - ) - - def test_serialized_collection(self) -> None: - """Test `serialized_collection` property.""" - assert self.test_round.serialized_collection == {} - - for payload in self.tx_payloads: - self.test_round.process_payload(payload) - - mcs_key = "packages.valory.skills.abstract_round_abci.test_tools.rounds.DummyTxPayload" - expected = { - f"agent_{i}": { - "_metaclass_registry_key": mcs_key, - "id_": self.tx_payloads[i].id_, - "round_count": self.tx_payloads[i].round_count, - "sender": self.tx_payloads[i].sender, - "value": self.tx_payloads[i].value, - "vote": self.tx_payloads[i].vote, - } - for i in range(4) - } - - assert self.test_round.serialized_collection == expected - - def test_run( - self, - ) -> None: - """Run tests.""" - - round_id = DummyCollectionRound.auto_round_id() - - # collection round may set a flag to allow payments from inactive agents (rejoin) - assert self.test_round._allow_rejoin_payloads is False # default - assert ( - self.test_round.accepting_payloads_from - == self.synchronized_data.participants - ) - self.test_round._allow_rejoin_payloads = True - assert ( - self.test_round.accepting_payloads_from - == self.synchronized_data.all_participants - ) - - first_payload, *_ = self.tx_payloads - self.test_round.process_payload(first_payload) - assert self.test_round.collection[first_payload.sender] == first_payload - - with pytest.raises( - ABCIAppInternalError, - match=f"internal error: sender agent_0 has already sent value for round: {round_id}", - ): - self.test_round.process_payload(first_payload) - - with pytest.raises( - ABCIAppInternalError, - match=re.escape( - "internal error: sender not in list of participants: ['agent_0', 'agent_1', 'agent_2', 'agent_3']" - ), - ): - self.test_round.process_payload(DummyTxPayload("sender", "value")) - - with pytest.raises( - TransactionNotValidError, - match=f"sender agent_0 has already sent value for round: {round_id}", - ): - self.test_round.check_payload(first_payload) - - with pytest.raises( - TransactionNotValidError, - match=re.escape( - "sender not in list of participants: ['agent_0', 'agent_1', 'agent_2', 'agent_3']" - ), - ): - self.test_round.check_payload(DummyTxPayload("sender", "value")) - - self._test_payload_with_wrong_round_count(self.test_round) - - -class TestCollectDifferentUntilAllRound(_BaseRoundTestClass): - """Test class for CollectDifferentUntilAllRound.""" - - def test_run( - self, - ) -> None: - """Run Tests.""" - - test_round = DummyCollectDifferentUntilAllRound( - synchronized_data=self.synchronized_data, - context=MagicMock(), - ) - round_id = DummyCollectDifferentUntilAllRound.auto_round_id() - - first_payload, *payloads = self.tx_payloads - test_round.process_payload(first_payload) - assert not test_round.collection_threshold_reached - - with pytest.raises( - ABCIAppInternalError, - match=f"internal error: sender agent_0 has already sent value for round: {round_id}", - ): - test_round.process_payload(first_payload) - - with pytest.raises( - TransactionNotValidError, - match=f"sender agent_0 has already sent value for round: {round_id}", - ): - test_round.check_payload(first_payload) - - with pytest.raises( - ABCIAppInternalError, - match="internal error: `CollectDifferentUntilAllRound` encountered a value '.*' that already exists.", - ): - object.__setattr__(first_payload, "sender", "other") - test_round.process_payload(first_payload) - - with pytest.raises( - TransactionNotValidError, - match="`CollectDifferentUntilAllRound` encountered a value '.*' that already exists.", - ): - test_round.check_payload(first_payload) - - for payload in payloads: - assert not test_round.collection_threshold_reached - test_round.process_payload(payload) - - assert test_round.collection_threshold_reached - self._test_payload_with_wrong_round_count(test_round) - - -class TestCollectSameUntilAllRound(_BaseRoundTestClass): - """Test class for CollectSameUntilAllRound.""" - - def test_run( - self, - ) -> None: - """Run Tests.""" - - test_round = DummyCollectSameUntilAllRound( - synchronized_data=self.synchronized_data, - context=MagicMock(), - ) - round_id = DummyCollectSameUntilAllRound.auto_round_id() - - first_payload, *payloads = [ - DummyTxPayload( - sender=agent, - value="test", - ) - for agent in sorted(self.participants) - ] - test_round.process_payload(first_payload) - assert not test_round.collection_threshold_reached - - with pytest.raises( - ABCIAppInternalError, - match="1 votes are not enough for `CollectSameUntilAllRound`", - ): - assert test_round.common_payload - - with pytest.raises( - ABCIAppInternalError, - match=f"internal error: sender agent_0 has already sent value for round: {round_id}", - ): - test_round.process_payload(first_payload) - - with pytest.raises( - TransactionNotValidError, - match=f"sender agent_0 has already sent value for round: {round_id}", - ): - test_round.check_payload(first_payload) - - with pytest.raises( - ABCIAppInternalError, - match="internal error: `CollectSameUntilAllRound` encountered a value '.*' " - "which is not the same as the already existing one: '.*'", - ): - bad_payload = DummyTxPayload( - sender="other", - value="other", - ) - test_round.process_payload(bad_payload) - - with pytest.raises( - TransactionNotValidError, - match="`CollectSameUntilAllRound` encountered a value '.*' " - "which is not the same as the already existing one: '.*'", - ): - test_round.check_payload(bad_payload) - - for payload in payloads: - assert not test_round.collection_threshold_reached - test_round.process_payload(payload) - - assert test_round.collection_threshold_reached - assert test_round.common_payload - self._test_payload_with_wrong_round_count(test_round, "test") - - -class TestCollectSameUntilThresholdRound(_BaseRoundTestClass): - """Test CollectSameUntilThresholdRound.""" - - @pytest.mark.parametrize( - "selection_key", - ("dummy_selection_key", tuple(f"dummy_selection_key_{i}" for i in range(2))), - ) - def test_run( - self, - selection_key: Union[str, Tuple[str, ...]], - ) -> None: - """Run tests.""" - - test_round = DummyCollectSameUntilThresholdRound( - synchronized_data=self.synchronized_data, - context=MagicMock(), - ) - test_round.collection_key = "dummy_collection_key" - test_round.selection_key = selection_key - assert test_round.end_block() is None - - first_payload, *payloads = get_dummy_tx_payloads( - self.participants, value="vote" - ) - test_round.process_payload(first_payload) - - assert not test_round.threshold_reached - with pytest.raises(ABCIAppInternalError, match="not enough votes"): - _ = test_round.most_voted_payload - - for payload in payloads: - test_round.process_payload(payload) - - assert test_round.threshold_reached - assert test_round.most_voted_payload == "vote" - - self._test_payload_with_wrong_round_count(test_round) - - test_round.done_event = DummyEvent.DONE - return_value = cast(Tuple[BaseSynchronizedData, Enum], test_round.end_block()) - assert return_value[-1] == test_round.done_event - - test_round.none_event = DummyEvent.NONE - test_round.collection.clear() - payloads = get_dummy_tx_payloads( - self.participants, value=None, is_value_none=True, is_vote_none=True - ) - for payload in payloads: - test_round.process_payload(payload) - assert test_round.most_voted_payload is None - return_value = cast(Tuple[BaseSynchronizedData, Enum], test_round.end_block()) - assert return_value[-1] == test_round.none_event - - test_round.no_majority_event = DummyEvent.NO_MAJORITY - test_round.collection.clear() - for participant in self.participants: - payload = DummyTxPayload(participant, value=participant) - test_round.process_payload(payload) - return_value = cast(Tuple[BaseSynchronizedData, Enum], test_round.end_block()) - assert return_value[-1] == test_round.no_majority_event - - def test_run_with_none( - self, - ) -> None: - """Run tests.""" - - test_round = DummyCollectSameUntilThresholdRound( - synchronized_data=self.synchronized_data, - context=MagicMock(), - ) - - first_payload, *payloads = get_dummy_tx_payloads( - self.participants, - value=None, - is_value_none=True, - ) - test_round.process_payload(first_payload) - - assert not test_round.threshold_reached - with pytest.raises(ABCIAppInternalError, match="not enough votes"): - _ = test_round.most_voted_payload - - for payload in payloads: - test_round.process_payload(payload) - - assert test_round.threshold_reached - assert test_round.most_voted_payload is None - - -class TestOnlyKeeperSendsRound(_BaseRoundTestClass, BaseOnlyKeeperSendsRoundTest): - """Test OnlyKeeperSendsRound.""" - - @pytest.mark.parametrize( - "payload_key", ("dummy_key", tuple(f"dummy_key_{i}" for i in range(2))) - ) - def test_run( - self, - payload_key: Union[str, Tuple[str, ...]], - ) -> None: - """Run tests.""" - - test_round = DummyOnlyKeeperSendsRound( - synchronized_data=self.synchronized_data.update( - most_voted_keeper_address="agent_0" - ), - context=MagicMock(), - ) - - assert test_round.keeper_payload is None - first_payload, *_ = self.tx_payloads - test_round.process_payload(first_payload) - assert test_round.keeper_payload is not None - - with pytest.raises( - ABCIAppInternalError, - match="internal error: keeper already set the payload.", - ): - test_round.process_payload(first_payload) - - with pytest.raises( - ABCIAppInternalError, - match=re.escape( - "internal error: sender not in list of participants: ['agent_0', 'agent_1', 'agent_2', 'agent_3']" - ), - ): - test_round.process_payload(DummyTxPayload(sender="sender", value="sender")) - - with pytest.raises( - ABCIAppInternalError, match="internal error: agent_1 not elected as keeper." - ): - test_round.process_payload(DummyTxPayload(sender="agent_1", value="sender")) - - with pytest.raises( - TransactionNotValidError, match="keeper payload value already set." - ): - test_round.check_payload(first_payload) - - with pytest.raises( - TransactionNotValidError, - match=re.escape( - "sender not in list of participants: ['agent_0', 'agent_1', 'agent_2', 'agent_3']" - ), - ): - test_round.check_payload(DummyTxPayload(sender="sender", value="sender")) - - with pytest.raises( - TransactionNotValidError, match="agent_1 not elected as keeper." - ): - test_round.check_payload(DummyTxPayload(sender="agent_1", value="sender")) - - self._test_payload_with_wrong_round_count(test_round) - - test_round.done_event = DummyEvent.DONE - test_round.payload_key = payload_key - assert test_round.end_block() - - def test_keeper_payload_is_none( - self, - ) -> None: - """Test keeper payload valur set to none.""" - - keeper = "agent_0" - self._complete_run( - self._test_round( - test_round=DummyOnlyKeeperSendsRound( - synchronized_data=self.synchronized_data.update( - most_voted_keeper_address=keeper, - ), - context=MagicMock(), - ), - keeper_payloads=DummyTxPayload(keeper, None), - synchronized_data_update_fn=lambda _synchronized_data, _test_round: _synchronized_data, - synchronized_data_attr_checks=[], - exit_event="FAIL_EVENT", - ) - ) - - -class TestVotingRound(_BaseRoundTestClass): - """Test VotingRound.""" - - def setup_test_voting_round(self) -> DummyVotingRound: - """Setup test voting round""" - return DummyVotingRound( - synchronized_data=self.synchronized_data, - context=MagicMock(), - ) - - def test_vote_count(self) -> None: - """Testing agent vote count""" - test_round = self.setup_test_voting_round() - a, b, c, d = self.participants - for agents, vote in [((a, d), True), ((c,), False), ((b,), None)]: - for payload in get_dummy_tx_payloads(frozenset(agents), vote=vote): - test_round.process_payload(payload) - assert dict(test_round.vote_count) == {True: 2, False: 1, None: 1} - - self._test_payload_with_wrong_round_count(test_round) - - @pytest.mark.parametrize("vote", [True, False, None]) - def test_threshold(self, vote: Optional[bool]) -> None: - """Runs threshold test.""" - - test_round = self.setup_test_voting_round() - test_round.collection_key = "dummy_collection_key" - test_round.done_event = DummyEvent.DONE - test_round.negative_event = DummyEvent.NEGATIVE - test_round.none_event = DummyEvent.NONE - - expected_threshold = { - True: lambda: test_round.positive_vote_threshold_reached, - False: lambda: test_round.negative_vote_threshold_reached, - None: lambda: test_round.none_vote_threshold_reached, - }[vote] - - expected_event = { - True: test_round.done_event, - False: test_round.negative_event, - None: test_round.none_event, - }[vote] - - first_payload, *payloads = get_dummy_tx_payloads(self.participants, vote=vote) - test_round.process_payload(first_payload) - assert test_round.end_block() is None - assert not expected_threshold() - for payload in payloads: - test_round.process_payload(payload) - assert expected_threshold() - return_value = cast(Tuple[BaseSynchronizedData, Enum], test_round.end_block()) - assert return_value[-1] == expected_event - - def test_end_round_no_majority(self) -> None: - """Test end round""" - - test_round = self.setup_test_voting_round() - test_round.no_majority_event = DummyEvent.NO_MAJORITY - for i, participant in enumerate(self.participants): - payload = DummyTxPayload(participant, value=participant, vote=bool(i % 2)) - test_round.process_payload(payload) - return_value = cast(Tuple[BaseSynchronizedData, Enum], test_round.end_block()) - assert return_value[-1] == test_round.no_majority_event - - def test_invalid_vote_payload_count(self) -> None: - """Testing agent vote count with invalid payload.""" - test_round = self.setup_test_voting_round() - a, b, c, d = self.participants - - class InvalidPayload(BaseTxPayload): - """InvalidPayload""" - - def get_dummy_tx_payloads_( - participants: FrozenSet[str], - ) -> List[BaseTxPayload]: - """Returns a list of DummyTxPayload objects.""" - return [InvalidPayload(sender=agent) for agent in sorted(participants)] - - for agents in [(a, d), (c,), (b,)]: - for payload in get_dummy_tx_payloads_(frozenset(agents)): - test_round.process_payload(payload) - - with pytest.raises(ValueError): - test_round.vote_count - - -class TestCollectDifferentUntilThresholdRound(_BaseRoundTestClass): - """Test CollectDifferentUntilThresholdRound.""" - - @pytest.mark.parametrize( - "required_confirmations", (MAX_PARTICIPANTS, MAX_PARTICIPANTS + 1) - ) - def test_run( - self, - required_confirmations: int, - ) -> None: - """Run tests.""" - - test_round = DummyCollectDifferentUntilThresholdRound( - synchronized_data=self.synchronized_data, - context=MagicMock(), - ) - test_round.block_confirmations = 0 - test_round.required_block_confirmations = required_confirmations - test_round.collection_key = "collection_key" - test_round.done_event = 0 - assert ( - test_round.synchronized_data.consensus_threshold <= required_confirmations - ), "Incorrect test parametrization: required confirmations cannot be set with a smalled value than the consensus threshold" - - first_payload, *payloads = get_dummy_tx_payloads(self.participants, vote=False) - test_round.process_payload(first_payload) - - assert not test_round.collection_threshold_reached - for payload in payloads: - test_round.process_payload(payload) - res = test_round.end_block() - assert test_round.block_confirmations <= required_confirmations - assert res is None - assert test_round.collection_threshold_reached - payloads_since_consensus = 2 - confirmations_remaining = required_confirmations - payloads_since_consensus - for _ in range(confirmations_remaining): - res = test_round.end_block() - assert test_round.block_confirmations <= required_confirmations - assert res is None - - res = test_round.end_block() - assert test_round.block_confirmations > required_confirmations - assert res is not None - assert res[1] == test_round.done_event - - assert test_round.collection_threshold_reached - self._test_payload_with_wrong_round_count(test_round) - - def test_end_round(self) -> None: - """Test end round""" - - test_round = DummyCollectDifferentUntilThresholdRound( - synchronized_data=self.synchronized_data, - context=MagicMock(), - ) - test_round.collection_key = "dummy_collection_key" - test_round.done_event = DummyEvent.DONE - - assert test_round.end_block() is None - for participant in self.participants: - payload = DummyTxPayload(participant, value=participant) - test_round.process_payload(payload) - return_value = cast(Tuple[BaseSynchronizedData, Enum], test_round.end_block()) - assert return_value[-1] == test_round.done_event - - -class TestCollectNonEmptyUntilThresholdRound(_BaseRoundTestClass): - """Test `CollectNonEmptyUntilThresholdRound`.""" - - def test_get_non_empty_values(self) -> None: - """Test `_get_non_empty_values`.""" - test_round = DummyCollectNonEmptyUntilThresholdRound( - synchronized_data=self.synchronized_data, - context=MagicMock(), - ) - payloads = get_dummy_tx_payloads(self.participants) - none_payload_idx = 3 - object.__setattr__(payloads[none_payload_idx], "value", None) - for payload in payloads: - test_round.process_payload(payload) - - non_empty_values = test_round._get_non_empty_values() - assert non_empty_values == { - tuple(sorted(self.participants))[i]: (f"agent_{i}", False) - if i != none_payload_idx - else (False,) - for i in range(4) - } - - self._test_payload_with_wrong_round_count(test_round) - - def test_process_payload(self) -> None: - """Test `process_payload`.""" - test_round = DummyCollectNonEmptyUntilThresholdRound( - synchronized_data=self.synchronized_data, - context=MagicMock(), - ) - first_payload, *payloads = get_dummy_tx_payloads(self.participants) - test_round.process_payload(first_payload) - - assert not test_round.collection_threshold_reached - for payload in payloads: - test_round.process_payload(payload) - - assert test_round.collection_threshold_reached - - @pytest.mark.parametrize( - "selection_key", - ("dummy_selection_key", tuple(f"dummy_selection_key_{i}" for i in range(2))), - ) - @pytest.mark.parametrize( - "is_value_none, expected_event", - ((True, DummyEvent.NONE), (False, DummyEvent.DONE)), - ) - def test_end_block( - self, - selection_key: Union[str, Tuple[str, ...]], - is_value_none: bool, - expected_event: str, - ) -> None: - """Test `end_block` when collection threshold is reached.""" - test_round = DummyCollectNonEmptyUntilThresholdRound( - synchronized_data=self.synchronized_data, - context=MagicMock(), - ) - test_round.selection_key = selection_key - payloads = get_dummy_tx_payloads( - self.participants, is_value_none=is_value_none, is_vote_none=True - ) - for payload in payloads: - test_round.process_payload(payload) - - test_round.collection = {f"test_{i}": payloads[i] for i in range(len(payloads))} - test_round.collection_key = "test" - test_round.done_event = DummyEvent.DONE - test_round.none_event = DummyEvent.NONE - - res = cast(Tuple[BaseSynchronizedData, Enum], test_round.end_block()) - assert res[0].db == self.synchronized_data.db - assert res[1] == expected_event diff --git a/packages/valory/skills/abstract_round_abci/tests/test_behaviours.py b/packages/valory/skills/abstract_round_abci/tests/test_behaviours.py deleted file mode 100644 index 1065189..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/test_behaviours.py +++ /dev/null @@ -1,951 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test the behaviours.py module of the skill.""" -# pylint: skip-file - -import platform -from abc import ABC -from calendar import timegm -from datetime import datetime -from pathlib import Path -from typing import Any, Dict, Generator, Optional, Tuple -from unittest import mock -from unittest.mock import MagicMock - -import pytest -from hypothesis import given, settings -from hypothesis import strategies as st - -from packages.valory.skills.abstract_round_abci import PUBLIC_ID -from packages.valory.skills.abstract_round_abci.base import ( - ABCIAppInternalError, - AbciApp, - AbstractRound, - BaseSynchronizedData, - BaseTxPayload, - DegenerateRound, - EventType, - OffenseType, - PendingOffense, - RoundSequence, -) -from packages.valory.skills.abstract_round_abci.behaviour_utils import ( - BaseBehaviour, - DegenerateBehaviour, - TmManager, -) -from packages.valory.skills.abstract_round_abci.behaviours import ( - AbstractRoundBehaviour, - PendingOffencesBehaviour, - _MetaRoundBehaviour, -) -from packages.valory.skills.abstract_round_abci.models import TendermintRecoveryParams -from packages.valory.skills.abstract_round_abci.tests.conftest import profile_name - - -BEHAVIOUR_A_ID = "behaviour_a" -BEHAVIOUR_B_ID = "behaviour_b" -BEHAVIOUR_C_ID = "behaviour_c" -CONCRETE_BACKGROUND_BEHAVIOUR_ID = "background_behaviour" -ROUND_A_ID = "round_a" -ROUND_B_ID = "round_b" -CONCRETE_BACKGROUND_ROUND_ID = "background_round" - - -settings.load_profile(profile_name) - - -def test_skill_public_id() -> None: - """Test skill module public ID""" - - assert PUBLIC_ID.name == Path(__file__).parents[1].name - assert PUBLIC_ID.author == Path(__file__).parents[3].name - - -class RoundA(AbstractRound): - """Round A.""" - - round_id = ROUND_A_ID - payload_class = BaseTxPayload - payload_attribute = "" - synchronized_data_class = BaseSynchronizedData - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, EventType]]: - """End block.""" - - def check_payload(self, payload: BaseTxPayload) -> None: - """Check payload.""" - - def process_payload(self, payload: BaseTxPayload) -> None: - """Process payload.""" - - -class RoundB(AbstractRound): - """Round B.""" - - round_id = ROUND_B_ID - payload_class = BaseTxPayload - payload_attribute = "" - synchronized_data_class = BaseSynchronizedData - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, EventType]]: - """End block.""" - - def check_payload(self, payload: BaseTxPayload) -> None: - """Check payload.""" - - def process_payload(self, payload: BaseTxPayload) -> None: - """Process payload.""" - - -class ConcreteBackgroundRound(AbstractRound): - """Concrete Background Round.""" - - round_id = ROUND_B_ID - payload_class = BaseTxPayload - payload_attribute = "" - synchronized_data_class = BaseSynchronizedData - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, EventType]]: - """End block.""" - - def check_payload(self, payload: BaseTxPayload) -> None: - """Check payload.""" - - def process_payload(self, payload: BaseTxPayload) -> None: - """Process payload.""" - - -class BehaviourA(BaseBehaviour): - """Dummy behaviour.""" - - behaviour_id = BEHAVIOUR_A_ID - matching_round = RoundA - - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Initialize behaviour.""" - super().__init__(*args, **kwargs) - self.count = 0 - - def setup(self) -> None: - """Setup behaviour.""" - self.count += 1 - - def async_act(self) -> Generator: - """Dummy act method.""" - yield - - -class BehaviourB(BaseBehaviour): - """Dummy behaviour.""" - - behaviour_id = BEHAVIOUR_B_ID - matching_round = RoundB - - def async_act(self) -> Generator: - """Dummy act method.""" - yield - - -class BehaviourC(BaseBehaviour, ABC): - """Dummy behaviour.""" - - matching_round = MagicMock() - - -def test_auto_behaviour_id() -> None: - """Test that the 'auto_behaviour_id()' method works as expected.""" - - assert BehaviourB.auto_behaviour_id() == BEHAVIOUR_B_ID - assert BehaviourB.behaviour_id == BEHAVIOUR_B_ID - assert BehaviourC.auto_behaviour_id() == "behaviour_c" - assert isinstance(BehaviourC.behaviour_id, property) - - -class ConcreteBackgroundBehaviour(BaseBehaviour): - """Dummy behaviour.""" - - behaviour_id = CONCRETE_BACKGROUND_BEHAVIOUR_ID - matching_round = ConcreteBackgroundRound - - def async_act(self) -> Generator: - """Dummy act method.""" - yield - - -class ConcreteAbciApp(AbciApp): - """Concrete ABCI App.""" - - initial_round_cls = RoundA - transition_function = {RoundA: {MagicMock(): RoundB}} - event_to_timeout: Dict = {} - - -class ConcreteRoundBehaviour(AbstractRoundBehaviour): - """Concrete round behaviour.""" - - abci_app_cls = ConcreteAbciApp - behaviours = {BehaviourA, BehaviourB} # type: ignore - initial_behaviour_cls = BehaviourA - background_behaviours_cls = {ConcreteBackgroundBehaviour} # type: ignore - - -class TestAbstractRoundBehaviour: - """Test 'AbstractRoundBehaviour' class.""" - - def setup(self) -> None: - """Set up the tests.""" - self.round_sequence_mock = MagicMock() - context_mock = MagicMock(params=MagicMock()) - context_mock.state.round_sequence = self.round_sequence_mock - context_mock.state.round_sequence.syncing_up = False - self.round_sequence_mock.block_stall_deadline_expired = False - self.behaviour = ConcreteRoundBehaviour(name="", skill_context=context_mock) - - @pytest.mark.parametrize("use_termination", (True, False)) - def test_setup(self, use_termination: bool) -> None: - """Test 'setup' method.""" - assert self.behaviour.background_behaviours == set() - self.behaviour.context.params.use_termination = use_termination - self.behaviour.setup() - assert self.behaviour.background_behaviours_cls == {ConcreteBackgroundBehaviour} - assert ( - isinstance( - self.behaviour.background_behaviours.pop(), ConcreteBackgroundBehaviour - ) - if use_termination - else self.behaviour.background_behaviours == set() - ) - - def test_teardown(self) -> None: - """Test 'teardown' method.""" - self.behaviour.teardown() - - def test_current_behaviour_return_none(self) -> None: - """Test 'current_behaviour' property return None.""" - assert self.behaviour.current_behaviour is None - - def test_act_current_behaviour_name_is_none(self) -> None: - """Test 'act' with current behaviour None.""" - self.behaviour.tm_manager = self.behaviour.instantiate_behaviour_cls(TmManager) # type: ignore - self.behaviour.current_behaviour = None - with mock.patch.object(self.behaviour, "_process_current_round"): - self.behaviour.act() - - @pytest.mark.parametrize( - "no_round, error", - ( - ( - True, - "Behaviour 'behaviour_without_round' specifies unknown 'unknown' as a matching round. " - "Please make sure that the round is implemented and belongs to the FSM. " - "If 'behaviour_without_round' is a background behaviour, please make sure that it is set correctly, " - "by overriding the corresponding attribute of the chained skill's behaviour.", - ), - (False, "round round_1 is not a matching round of any behaviour"), - ), - ) - def test_check_matching_round_consistency_no_behaviour( - self, no_round: bool, error: str - ) -> None: - """Test classmethod '_check_matching_round_consistency', when no behaviour or round is specified.""" - rounds = [ - MagicMock(**{"auto_round_id.return_value": f"round_{i}"}) for i in range(3) - ] - mock_behaviours = [ - MagicMock(matching_round=round, behaviour_id=f"behaviour_{i}") - for i, round in enumerate(rounds[2:]) - ] - if no_round: - mock_behaviours.append( - MagicMock( - matching_round="unknown", behaviour_id="behaviour_without_round" - ) - ) - - with mock.patch.object( - _MetaRoundBehaviour, "_check_all_required_classattributes_are_set" - ), mock.patch.object( - _MetaRoundBehaviour, "_check_behaviour_id_uniqueness" - ), mock.patch.object( - _MetaRoundBehaviour, "_check_initial_behaviour_in_set_of_behaviours" - ), pytest.raises( - ABCIAppInternalError, - match=error, - ): - - class MyRoundBehaviour(AbstractRoundBehaviour): - abci_app_cls = MagicMock( - get_all_round_classes=lambda _, include_background_rounds: rounds, - final_states={ - rounds[0], - }, - ) - behaviours = mock_behaviours # type: ignore - initial_behaviour_cls = MagicMock() - - def test_check_matching_round_consistency(self) -> None: - """Test classmethod '_check_matching_round_consistency', negative case.""" - rounds = [ - MagicMock(**{"auto_round_id.return_value": f"round_{i}"}) for i in range(3) - ] - mock_behaviours = [ - MagicMock(matching_round=round, behaviour_id=f"behaviour_{i}") - for i, round in enumerate(rounds) - ] - - with mock.patch.object( - _MetaRoundBehaviour, "_check_all_required_classattributes_are_set" - ), mock.patch.object( - _MetaRoundBehaviour, "_check_behaviour_id_uniqueness" - ), mock.patch.object( - _MetaRoundBehaviour, "_check_initial_behaviour_in_set_of_behaviours" - ), pytest.raises( - ABCIAppInternalError, - match="internal error: round round_0 is a final round it shouldn't have any matching behaviours", - ): - - class MyRoundBehaviour(AbstractRoundBehaviour): - abci_app_cls = MagicMock( - get_all_round_classes=lambda _, include_background_rounds: rounds, - final_states={ - rounds[0], - }, - ) - behaviours = mock_behaviours # type: ignore - initial_behaviour_cls = MagicMock() - - @pytest.mark.parametrize("behaviour_cls", (set(), {MagicMock()})) - def test_check_matching_round_consistency_with_bg_rounds( - self, behaviour_cls: set - ) -> None: - """Test classmethod '_check_matching_round_consistency' when a background behaviour class is set.""" - rounds = [ - MagicMock(**{"auto_round_id.return_value": f"round_{i}"}) for i in range(3) - ] - mock_behaviours = ( - [ - MagicMock(matching_round=round_, behaviour_id=f"behaviour_{i}") - for i, round_ in enumerate(rounds[1:]) - ] - if behaviour_cls - else [] - ) - - with mock.patch.object( - _MetaRoundBehaviour, "_check_all_required_classattributes_are_set" - ), mock.patch.object( - _MetaRoundBehaviour, "_check_behaviour_id_uniqueness" - ), mock.patch.object( - _MetaRoundBehaviour, "_check_initial_behaviour_in_set_of_behaviours" - ): - - class MyRoundBehaviour(AbstractRoundBehaviour): - abci_app_cls = MagicMock( - get_all_round_classes=lambda _, include_background_rounds: rounds - if include_background_rounds - else [], - final_states={ - rounds[0], - } - if behaviour_cls - else {}, - ) - behaviours = mock_behaviours # type: ignore - initial_behaviour_cls = MagicMock() - background_behaviours_cls = behaviour_cls - - def test_get_behaviour_id_to_behaviour_mapping_negative(self) -> None: - """Test classmethod '_get_behaviour_id_to_behaviour_mapping', negative case.""" - behaviour_id = "behaviour_id" - behaviour_1 = MagicMock(**{"auto_behaviour_id.return_value": behaviour_id}) - behaviour_2 = MagicMock(**{"auto_behaviour_id.return_value": behaviour_id}) - - with pytest.raises( - ValueError, - match=f"cannot have two behaviours with the same id; got {behaviour_2} and {behaviour_1} both with id '{behaviour_id}'", - ): - with mock.patch.object(_MetaRoundBehaviour, "_check_consistency"): - - class MyRoundBehaviour(AbstractRoundBehaviour): - abci_app_cls = MagicMock - behaviours = [behaviour_1, behaviour_2] # type: ignore - initial_behaviour_cls = MagicMock() - - MyRoundBehaviour(name=MagicMock(), skill_context=MagicMock()) - - def test_get_round_to_behaviour_mapping_two_behaviours_same_round(self) -> None: - """Test classmethod '_get_round_to_behaviour_mapping' when two different behaviours point to the same round.""" - behaviour_id_1 = "behaviour_id_1" - behaviour_id_2 = "behaviour_id_2" - round_cls = RoundA - round_id = round_cls.auto_round_id() - behaviour_1 = MagicMock( - matching_round=round_cls, - **{"auto_behaviour_id.return_value": behaviour_id_1}, - ) - behaviour_2 = MagicMock( - matching_round=round_cls, - **{"auto_behaviour_id.return_value": behaviour_id_2}, - ) - - with pytest.raises( - ValueError, - match=f"the behaviours '{behaviour_2.auto_behaviour_id()}' and '{behaviour_1.auto_behaviour_id()}' point to the same matching round '{round_id}'", - ): - with mock.patch.object(_MetaRoundBehaviour, "_check_consistency"): - - class MyRoundBehaviour(AbstractRoundBehaviour): - abci_app_cls = ConcreteAbciApp - behaviours = [behaviour_1, behaviour_2] # type: ignore - initial_behaviour_cls = behaviour_1 - - MyRoundBehaviour(name=MagicMock(), skill_context=MagicMock()) - - def test_get_round_to_behaviour_mapping_with_final_rounds(self) -> None: - """Test classmethod '_get_round_to_behaviour_mapping' with final rounds.""" - - class FinalRound(DegenerateRound, ABC): - """A final round for testing.""" - - behaviour_id_1 = "behaviour_id_1" - behaviour_1 = MagicMock(behaviour_id=behaviour_id_1, matching_round=RoundA) - - class AbciAppTest(AbciApp): - """Abci App for testing.""" - - initial_round_cls = RoundA - transition_function = {RoundA: {MagicMock(): FinalRound}, FinalRound: {}} - event_to_timeout: Dict = {} - final_states = {FinalRound} - - class MyRoundBehaviour(AbstractRoundBehaviour): - abci_app_cls = AbciAppTest - behaviours = {behaviour_1} - initial_behaviour_cls = behaviour_1 - matching_round = FinalRound - - behaviour = MyRoundBehaviour(name=MagicMock(), skill_context=MagicMock()) - final_behaviour = behaviour._round_to_behaviour[FinalRound] - assert issubclass(final_behaviour, DegenerateBehaviour) - assert ( - final_behaviour.auto_behaviour_id() - == f"degenerate_behaviour_{FinalRound.auto_round_id()}" - ) - - def test_check_behaviour_id_uniqueness_negative(self) -> None: - """Test metaclass method '_check_consistency', negative case.""" - behaviour_id = "behaviour_id" - behaviour_1_cls_name = "Behaviour1" - behaviour_2_cls_name = "Behaviour2" - behaviour_1 = MagicMock( - __name__=behaviour_1_cls_name, - **{"auto_behaviour_id.return_value": behaviour_id}, - ) - behaviour_2 = MagicMock( - __name__=behaviour_2_cls_name, - **{"auto_behaviour_id.return_value": behaviour_id}, - ) - - with pytest.raises( - ABCIAppInternalError, - match=rf"behaviours \['{behaviour_1_cls_name}', '{behaviour_2_cls_name}'\] have the same behaviour id '{behaviour_id}'", - ): - - class MyRoundBehaviour(AbstractRoundBehaviour): - abci_app_cls = MagicMock - behaviours = [behaviour_1, behaviour_2] # type: ignore - initial_behaviour_cls = MagicMock() - - def test_check_consistency_two_behaviours_same_round(self) -> None: - """Test metaclass method '_check_consistency' when two different behaviours point to the same round.""" - behaviour_id_1 = "behaviour_id_1" - behaviour_id_2 = "behaviour_id_2" - round_cls = RoundA - round_id = round_cls.auto_round_id() - behaviour_1 = MagicMock( - matching_round=round_cls, - **{"auto_behaviour_id.return_value": "behaviour_id_1"}, - ) - behaviour_2 = MagicMock( - matching_round=round_cls, - **{"auto_behaviour_id.return_value": "behaviour_id_2"}, - ) - - with pytest.raises( - ABCIAppInternalError, - match=rf"internal error: behaviours \['{behaviour_id_1}', '{behaviour_id_2}'\] have the same matching round '{round_id}'", - ): - - class MyRoundBehaviour(AbstractRoundBehaviour): - abci_app_cls = ConcreteAbciApp - behaviours = [behaviour_1, behaviour_2] # type: ignore - initial_behaviour_cls = behaviour_1 - - def test_check_initial_behaviour_in_set_of_behaviours_negative_case(self) -> None: - """Test classmethod '_check_initial_behaviour_in_set_of_behaviours' when initial behaviour is NOT in the set.""" - behaviour_1 = MagicMock( - matching_round=MagicMock(), - **{"auto_behaviour_id.return_value": "behaviour_id_1"}, - ) - behaviour_2 = MagicMock( - matching_round=MagicMock(), - **{"auto_behaviour_id.return_value": "behaviour_id_2"}, - ) - - with pytest.raises( - ABCIAppInternalError, - match=f"initial behaviour {behaviour_2.auto_behaviour_id()} is not in the set of behaviours", - ): - - class MyRoundBehaviour(AbstractRoundBehaviour): - abci_app_cls = ConcreteAbciApp - behaviours = {behaviour_1} - initial_behaviour_cls = behaviour_2 - - def test_act_no_round_change(self) -> None: - """Test the 'act' method of the behaviour, with no round change.""" - self.round_sequence_mock.current_round = RoundA(MagicMock(), MagicMock()) - self.round_sequence_mock.current_round_height = 0 - - # check that after setup(), current behaviour is initial behaviour - self.behaviour.setup() - assert isinstance(self.behaviour.current_behaviour, BehaviourA) - - with mock.patch.object( - self.behaviour.current_behaviour, "clean_up" - ) as clean_up_mock: - # check that after act(), current behaviour is initial behaviour and `clean_up()` has not been called - self.behaviour.act() - assert isinstance(self.behaviour.current_behaviour, BehaviourA) - clean_up_mock.assert_not_called() - - # check that once the flag done is set, the `clean_up()` has been called - # and `current_behaviour` is set to `None`. - self.behaviour.current_behaviour.set_done() - self.behaviour.act() - assert self.behaviour.current_behaviour is None - clean_up_mock.assert_called_once() - - def test_act_behaviour_setup(self) -> None: - """Test the 'act' method of the FSM behaviour triggers setup() of the behaviour.""" - self.round_sequence_mock.current_round = RoundA(MagicMock(), MagicMock()) - self.round_sequence_mock.current_round_height = 0 - - # check that after setup(), current behaviour is initial behaviour - self.behaviour.setup() - assert isinstance(self.behaviour.current_behaviour, BehaviourA) - - assert self.behaviour.current_behaviour.count == 0 - - with mock.patch.object( - self.behaviour.current_behaviour, "clean_up" - ) as clean_up_mock: - # check that after act() first time, a call to setup has been made - self.behaviour.act() - assert isinstance(self.behaviour.current_behaviour, BehaviourA) - assert self.behaviour.current_behaviour.count == 1 - - # check that after act() second time, no further call to setup - self.behaviour.act() - assert self.behaviour.current_behaviour.count == 1 - - # check that the `clean_up()` has not been called - clean_up_mock.assert_not_called() - - def test_act_with_round_change(self) -> None: - """Test the 'act' method of the behaviour, with round change.""" - self.round_sequence_mock.current_round = RoundA(MagicMock(), MagicMock()) - self.round_sequence_mock.current_round_height = 0 - - # check that after setup(), current behaviour is initial behaviour - self.behaviour.setup() - assert isinstance(self.behaviour.current_behaviour, BehaviourA) - - # check that after act(), current behaviour is initial behaviour - with mock.patch.object( - self.behaviour.current_behaviour, "clean_up" - ) as clean_up_mock: - self.behaviour.act() - assert isinstance(self.behaviour.current_behaviour, BehaviourA) - clean_up_mock.assert_not_called() - - # change the round - self.round_sequence_mock.current_round = RoundB(MagicMock(), MagicMock()) - self.round_sequence_mock.current_round_height = ( - self.round_sequence_mock.current_round_height + 1 - ) - - # check that if the round is changed, the behaviour transition is performed and the clean-up is called - self.behaviour.act() - assert isinstance(self.behaviour.current_behaviour, BehaviourB) - clean_up_mock.assert_called_once() - - def test_act_with_round_change_after_current_behaviour_is_none(self) -> None: - """Test the 'act' method of the behaviour, with round change, after cur behaviour is none.""" - self.behaviour.tm_manager = self.behaviour.instantiate_behaviour_cls(TmManager) # type: ignore - self.round_sequence_mock.current_round = RoundA(MagicMock(), MagicMock()) - self.round_sequence_mock.current_round_height = 0 - - # instantiate behaviour - self.behaviour.current_behaviour = self.behaviour.instantiate_behaviour_cls( - BehaviourA - ) - - with mock.patch.object( - self.behaviour.current_behaviour, "clean_up" - ) as clean_up_mock: - # check that after act(), current behaviour is same behaviour - self.behaviour.act() - assert isinstance(self.behaviour.current_behaviour, BehaviourA) - clean_up_mock.assert_not_called() - - # check that after the behaviour is done, current behaviour is None - self.behaviour.current_behaviour.set_done() - self.behaviour.act() - assert self.behaviour.current_behaviour is None - clean_up_mock.assert_called_once() - - # change the round - self.round_sequence_mock.current_round = RoundB(MagicMock(), MagicMock()) - self.round_sequence_mock.current_round_height = ( - self.round_sequence_mock.current_round_height + 1 - ) - - # check that if the round is changed, the behaviour transition is taken - self.behaviour.act() - assert isinstance(self.behaviour.current_behaviour, BehaviourB) - clean_up_mock.assert_called_once() - - @mock.patch.object( - AbstractRoundBehaviour, - "_process_current_round", - ) - @mock.patch.object( - TmManager, - "tm_communication_unhealthy", - new_callable=mock.PropertyMock, - return_value=False, - ) - @mock.patch.object( - TmManager, - "is_acting", - new_callable=mock.PropertyMock, - return_value=False, - ) - @pytest.mark.parametrize("expected_termination_acting", (True, False)) - def test_termination_behaviour_acting( - self, - _: mock._patch, - __: mock._patch, - ___: mock._patch, - expected_termination_acting: bool, - ) -> None: - """Test if the termination background behaviour is acting only when it should.""" - self.behaviour.context.params.use_termination = expected_termination_acting - self.behaviour.setup() - if expected_termination_acting: - with mock.patch.object( - ConcreteBackgroundBehaviour, - "act_wrapper", - ) as mock_background_act: - self.behaviour.act() - mock_background_act.assert_called() - else: - assert self.behaviour.background_behaviours == set() - - @mock.patch.object( - AbstractRoundBehaviour, - "_process_current_round", - ) - @pytest.mark.parametrize( - ("mock_tm_communication_unhealthy", "mock_is_acting", "expected_fix"), - [ - (True, True, True), - (False, True, True), - (True, False, True), - (False, False, False), - ], - ) - def test_try_fix_call( - self, - _: mock._patch, - mock_tm_communication_unhealthy: bool, - mock_is_acting: bool, - expected_fix: bool, - ) -> None: - """Test that `try_fix` is called when necessary.""" - self.behaviour.tm_manager = self.behaviour.instantiate_behaviour_cls(TmManager) # type: ignore - with mock.patch.object( - TmManager, - "tm_communication_unhealthy", - new_callable=mock.PropertyMock, - return_value=mock_tm_communication_unhealthy, - ), mock.patch.object( - TmManager, - "is_acting", - new_callable=mock.PropertyMock, - return_value=mock_is_acting, - ), mock.patch.object( - TmManager, - "try_fix", - ) as mock_try_fix: - self.behaviour.act() - if expected_fix: - mock_try_fix.assert_called() - else: - mock_try_fix.assert_not_called() - - -def test_meta_round_behaviour_when_instance_not_subclass_of_abstract_round_behaviour() -> ( - None -): - """Test instantiation of meta class when instance not a subclass of abstract round behaviour.""" - - class MyRoundBehaviour(metaclass=_MetaRoundBehaviour): - pass - - -def test_abstract_round_behaviour_instantiation_without_attributes_raises_error() -> ( - None -): - """Test that definition of concrete subclass of AbstractRoundBehavior without attributes raises error.""" - with pytest.raises(ABCIAppInternalError): - - class MyRoundBehaviour(AbstractRoundBehaviour): - pass - - -def test_abstract_round_behaviour_matching_rounds_not_covered() -> None: - """Test that definition of concrete subclass of AbstractRoundBehavior when matching round not covered.""" - with pytest.raises(ABCIAppInternalError): - - class MyRoundBehaviour(AbstractRoundBehaviour): - abci_app_cls = ConcreteAbciApp - behaviours = {BehaviourA} - initial_behaviour_cls = BehaviourA - - -@mock.patch.object( - BaseBehaviour, - "tm_communication_unhealthy", - new_callable=mock.PropertyMock, - return_value=False, -) -def test_self_loops_in_abci_app_reinstantiate_behaviour(_: mock._patch) -> None: - """Test that a self-loop transition in the AbciApp will trigger a transition in the round behaviour.""" - event = MagicMock() - - class AbciAppTest(AbciApp): - initial_round_cls = RoundA - transition_function = {RoundA: {event: RoundA}} - - class RoundBehaviour(AbstractRoundBehaviour): - abci_app_cls = AbciAppTest - behaviours = {BehaviourA} - initial_behaviour_cls = BehaviourA - - round_sequence = RoundSequence(MagicMock(), AbciAppTest) - round_sequence.end_sync() - round_sequence.setup(MagicMock(), MagicMock()) - context_mock = MagicMock() - context_mock.state.round_sequence = round_sequence - behaviour = RoundBehaviour(name="", skill_context=context_mock) - behaviour.setup() - - behaviour_1 = behaviour.current_behaviour - assert isinstance(behaviour_1, BehaviourA) - - round_sequence.abci_app.process_event(event) - - behaviour.act() - behaviour_2 = behaviour.current_behaviour - assert isinstance(behaviour_2, BehaviourA) - assert id(behaviour_1) != id(behaviour_2) - assert behaviour_1 != behaviour_2 - - -class LongRunningBehaviour(BaseBehaviour): - """A behaviour that runs forevever.""" - - behaviour_id = "long_running_behaviour" - matching_round = RoundA - - def async_act(self) -> Generator: - """An act method that simply cycles forever.""" - while True: - # cycle forever - yield - - -def test_reset_should_be_performed_when_tm_unhealthy() -> None: - """Test that hard reset is performed while a behaviour is running, and tendermint communication is unhealthy.""" - event = MagicMock() - - class AbciAppTest(AbciApp): - initial_round_cls = RoundA - transition_function = {RoundA: {event: RoundA}} - - class RoundBehaviour(AbstractRoundBehaviour): - abci_app_cls = AbciAppTest - behaviours = {LongRunningBehaviour} # type: ignore - initial_behaviour_cls = LongRunningBehaviour - - round_sequence = RoundSequence(MagicMock(), AbciAppTest) - round_sequence.end_sync() - round_sequence.setup(MagicMock(), MagicMock()) - context_mock = MagicMock() - context_mock.state.round_sequence = round_sequence - tm_recovery_params = TendermintRecoveryParams( - reset_from_round=RoundA.auto_round_id() - ) - context_mock.state.get_acn_result = MagicMock(return_value=tm_recovery_params) - context_mock.params.ipfs_domain_name = None - behaviour = RoundBehaviour(name="", skill_context=context_mock) - behaviour.setup() - - current_behaviour = behaviour.current_behaviour - assert isinstance(current_behaviour, LongRunningBehaviour) - - # upon entering the behaviour, the tendermint node communication is working well - with mock.patch.object( - RoundSequence, - "block_stall_deadline_expired", - new_callable=mock.PropertyMock, - return_value=False, - ): - behaviour.act() - - def dummy_num_peers( - timeout: Optional[float] = None, - ) -> Generator[None, None, Optional[int]]: - """A dummy method for num_active_peers.""" - # a None response is acceptable here, because tendermint is not healthy - return None - yield - - def dummy_reset_tendermint_with_wait( - on_startup: bool = False, - is_recovery: bool = False, - ) -> Generator[None, None, bool]: - """A dummy method for reset_tendermint_with_wait.""" - # we assume the reset goes through successfully - return True - yield - - # at this point LongRunningBehaviour is running - # while the behaviour is running, the tendermint node - # becomes unhealthy, we expect the node to be reset - with mock.patch.object( - RoundSequence, - "block_stall_deadline_expired", - new_callable=mock.PropertyMock, - return_value=True, - ), mock.patch.object( - BaseBehaviour, - "num_active_peers", - side_effect=dummy_num_peers, - ), mock.patch.object( - BaseBehaviour, - "reset_tendermint_with_wait", - side_effect=dummy_reset_tendermint_with_wait, - ) as mock_reset_tendermint: - behaviour.tm_manager.synchronized_data.max_participants = 3 # type: ignore - assert behaviour.tm_manager is not None - behaviour.tm_manager.gentle_reset_attempted = True - behaviour.act() - mock_reset_tendermint.assert_called() - - -class TestPendingOffencesBehaviour: - """Tests for `PendingOffencesBehaviour`.""" - - behaviour: PendingOffencesBehaviour - - @classmethod - def setup_class(cls) -> None: - """Setup the test class.""" - cls.behaviour = PendingOffencesBehaviour( - name="test", - skill_context=MagicMock(), - ) - - @pytest.mark.skipif( - platform.system() == "Windows", - reason="`timegm` behaves differently on Windows. " - "As a result, the generation of `last_transition_timestamp` is invalid.", - ) - @given( - offence=st.builds( - PendingOffense, - accused_agent_address=st.text(), - round_count=st.integers(min_value=0), - offense_type=st.sampled_from(OffenseType), - last_transition_timestamp=st.floats( - min_value=timegm(datetime(1971, 1, 1).utctimetuple()), - max_value=timegm(datetime(8000, 1, 1).utctimetuple()) - 2000, - ), - time_to_live=st.floats(min_value=1, max_value=2000), - ), - wait_ticks=st.integers(min_value=0, max_value=1000), - expired=st.booleans(), - ) - def test_pending_offences_act( - self, - offence: PendingOffense, - wait_ticks: int, - expired: bool, - ) -> None: - """Test `PendingOffencesBehaviour`.""" - offence_expiration = offence.last_transition_timestamp + offence.time_to_live - offence_expiration += 1 if expired else -1 - self.behaviour.round_sequence.last_round_transition_timestamp = datetime.fromtimestamp( # type: ignore - offence_expiration - ) - - gen = self.behaviour.async_act() - - with mock.patch.object( - self.behaviour, - "send_a2a_transaction", - ) as mock_send_a2a_transaction, mock.patch.object( - self.behaviour, - "wait_until_round_end", - ) as mock_wait_until_round_end, mock.patch.object( - self.behaviour, - "set_done", - ) as mock_set_done: - # while pending offences are empty, the behaviour simply waits - for _ in range(wait_ticks): - next(gen) - - self.behaviour.round_sequence.pending_offences = {offence} - - with pytest.raises(StopIteration): - next(gen) - - check = "assert_not_called" if expired else "assert_called_once" - - for mocked in ( - mock_send_a2a_transaction, - mock_wait_until_round_end, - mock_set_done, - ): - getattr(mocked, check)() diff --git a/packages/valory/skills/abstract_round_abci/tests/test_behaviours_utils.py b/packages/valory/skills/abstract_round_abci/tests/test_behaviours_utils.py deleted file mode 100644 index 1991586..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/test_behaviours_utils.py +++ /dev/null @@ -1,2681 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test the behaviours_utils.py module of the skill.""" - -import json -import logging -import platform -import time -from abc import ABC -from datetime import datetime -from enum import Enum -from pathlib import Path -from typing import ( - Any, - Callable, - Dict, - Generator, - List, - Optional, - Tuple, - Type, - Union, - cast, -) -from unittest import mock -from unittest.mock import MagicMock - -import pytest -import pytz # pylint: disable=import-error -from _pytest.logging import LogCaptureFixture - -# pylint: skip-file -from aea.common import JSONLike -from aea.protocols.base import Message -from aea.test_tools.utils import as_context -from aea_test_autonomy.helpers.base import try_send -from hypothesis import given, settings -from hypothesis import strategies as st - -from packages.open_aea.protocols.signing import SigningMessage -from packages.valory.connections.http_client.connection import HttpDialogues -from packages.valory.connections.ipfs.connection import IpfsDialogues -from packages.valory.connections.ipfs.connection import PUBLIC_ID as IPFS_CONNECTION_ID -from packages.valory.protocols.http import HttpMessage -from packages.valory.protocols.ipfs import IpfsMessage -from packages.valory.protocols.ipfs.dialogues import IpfsDialogue -from packages.valory.protocols.ledger_api.custom_types import ( - SignedTransaction, - SignedTransactions, - TransactionDigest, - TransactionDigests, -) -from packages.valory.protocols.ledger_api.message import LedgerApiMessage -from packages.valory.protocols.tendermint import TendermintMessage -from packages.valory.skills.abstract_round_abci import behaviour_utils -from packages.valory.skills.abstract_round_abci.base import ( - AbstractRound, - BaseSynchronizedData, - BaseTxPayload, - DegenerateRound, - LEDGER_API_ADDRESS, - OK_CODE, - Transaction, -) -from packages.valory.skills.abstract_round_abci.behaviour_utils import ( - AsyncBehaviour, - BaseBehaviour, - BaseBehaviourInternalError, - DegenerateBehaviour, - GENESIS_TIME_FMT, - INITIAL_HEIGHT, - IPFSBehaviour, - NON_200_RETURN_CODE_DURING_RESET_THRESHOLD, - RPCResponseStatus, - SendException, - TimeoutException, - TmManager, - _MetaBaseBehaviour, - make_degenerate_behaviour, -) -from packages.valory.skills.abstract_round_abci.io_.ipfs import ( - IPFSInteract, - IPFSInteractionError, -) -from packages.valory.skills.abstract_round_abci.models import ( - SharedState, - TendermintRecoveryParams, -) -from packages.valory.skills.abstract_round_abci.tests.conftest import profile_name - - -_DEFAULT_REQUEST_TIMEOUT = 10.0 -_DEFAULT_REQUEST_RETRY_DELAY = 1.0 -_DEFAULT_TX_MAX_ATTEMPTS = 10 -_DEFAULT_TX_TIMEOUT = 10.0 - -settings.load_profile(profile_name) - - -PACKAGE_DIR = Path(__file__).parent.parent - -# https://github.com/python/cpython/issues/94414 -# https://stackoverflow.com/questions/46133223/maximum-value-of-timestamp -# NOTE: timezone in behaviour_utils._get_reset_params set to UTC -# but hypothesis does not allow passing of the `tzinfo` argument -# hence we add and subtract a day from the actual min / max datetime -MIN_DATETIME_WINDOWS = datetime(1970, 1, 3, 1, 0, 0) -MAX_DATETIME_WINDOWS = datetime(3000, 12, 30, 23, 59, 59) - - -def mock_yield_and_return( - return_value: Any, -) -> Callable[[], Generator[None, None, Any]]: - """Wrapper for a Dummy generator that returns a `bool`.""" - - def yield_and_return(*_: Any, **__: Any) -> Generator[None, None, Any]: - """Dummy generator that returns a `bool`.""" - yield - return return_value - - return yield_and_return - - -def yield_and_return_bool_wrapper( - flag_value: bool, -) -> Callable[[], Generator[None, None, Optional[bool]]]: - """Wrapper for a Dummy generator that returns a `bool`.""" - - def yield_and_return_bool( - **_: bool, - ) -> Generator[None, None, Optional[bool]]: - """Dummy generator that returns a `bool`.""" - yield - return flag_value - - return yield_and_return_bool - - -def yield_and_return_int_wrapper( - value: Optional[int], -) -> Callable[[], Generator[None, None, Optional[int]]]: - """Wrapper for a Dummy generator that returns an `int`.""" - - def yield_and_return_int( - **_: int, - ) -> Generator[None, None, Optional[int]]: - """Dummy generator that returns an `int`.""" - yield - return value - - return yield_and_return_int - - -class AsyncBehaviourTest(AsyncBehaviour, ABC): - """Concrete AsyncBehaviour class for testing purposes.""" - - def async_act_wrapper(self) -> Generator: - """Do async act wrapper. Forwards to 'async_act'.""" - yield from self.async_act() - - def async_act(self) -> Generator: - """Do 'async_act'.""" - yield None - - -def test_async_behaviour_ticks() -> None: - """Test "AsyncBehaviour", only ticks.""" - - class MyAsyncBehaviour(AsyncBehaviourTest): - counter = 0 - - def async_act(self) -> Generator: - self.counter += 1 - yield - self.counter += 1 - yield - self.counter += 1 - - behaviour = MyAsyncBehaviour() - assert behaviour.counter == 0 - behaviour.act() - assert behaviour.counter == 1 - assert behaviour.state == AsyncBehaviour.AsyncState.RUNNING - behaviour.act() - assert behaviour.counter == 2 - assert behaviour.state == AsyncBehaviour.AsyncState.RUNNING - behaviour.act() - assert behaviour.counter == 3 - assert behaviour.state == AsyncBehaviour.AsyncState.READY - - -def test_async_behaviour_wait_for_message() -> None: - """Test 'wait_for_message'.""" - - expected_message = "message" - - class MyAsyncBehaviour(AsyncBehaviourTest): - counter = 0 - message = None - - def async_act(self) -> Generator: - self.counter += 1 - self.message = yield from self.wait_for_message( - lambda message: message == expected_message - ) - self.counter += 1 - - behaviour = MyAsyncBehaviour() - assert behaviour.counter == 0 - behaviour.act() - assert behaviour.counter == 1 - assert behaviour.state == AsyncBehaviour.AsyncState.WAITING_MESSAGE - - # another call to act doesn't change the state (still waiting for message) - behaviour.act() - assert behaviour.counter == 1 - assert behaviour.state == AsyncBehaviour.AsyncState.WAITING_MESSAGE - - # sending a message that does not satisfy the condition won't change state - behaviour.try_send("wrong_message") - behaviour.act() - assert behaviour.counter == 1 - assert behaviour.state == AsyncBehaviour.AsyncState.WAITING_MESSAGE - - # sending a message before it is processed raises an exception - behaviour.try_send("wrong_message") - with pytest.raises(SendException, match="cannot send message"): - behaviour.try_send("wrong_message") - behaviour.act() - - # sending the right message will transition to the next state, - # but only when calling act() - behaviour.try_send(expected_message) - assert behaviour.counter == 1 - assert behaviour.state == AsyncBehaviour.AsyncState.WAITING_MESSAGE - behaviour.act() - assert behaviour.counter == 2 - assert behaviour.message == expected_message - assert behaviour.state == AsyncBehaviour.AsyncState.READY - - -def test_async_behaviour_wait_for_message_raises_timeout_exception() -> None: - """Test 'wait_for_message' when it raises TimeoutException.""" - - with pytest.raises(TimeoutException): - behaviour = AsyncBehaviourTest() - gen = behaviour.wait_for_message(lambda _: False, timeout=0.01) - # trigger function - try_send(gen) - # sleep so to run out the timeout - time.sleep(0.02) - # trigger function and make the exception to raise - try_send(gen) - - -def test_async_behaviour_wait_for_condition() -> None: - """Test 'wait_for_condition' method.""" - - condition = False - - class MyAsyncBehaviour(AsyncBehaviourTest): - counter = 0 - - def async_act(self) -> Generator: - self.counter += 1 - yield from self.wait_for_condition(lambda: condition) - self.counter += 1 - - behaviour = MyAsyncBehaviour() - assert behaviour.counter == 0 - behaviour.act() - assert behaviour.counter == 1 - assert behaviour.state == AsyncBehaviour.AsyncState.RUNNING - - # if condition is false, execution remains at the same point - behaviour.act() - assert behaviour.counter == 1 - assert behaviour.state == AsyncBehaviour.AsyncState.RUNNING - - # if condition is true, execution continues - condition = True - behaviour.act() - assert behaviour.counter == 2 - assert behaviour.state == AsyncBehaviour.AsyncState.READY - - -def test_async_behaviour_wait_for_condition_with_timeout() -> None: - """Test 'wait_for_condition' method with timeout expired.""" - - class MyAsyncBehaviour(AsyncBehaviourTest): - counter = 0 - - def async_act(self) -> Generator: - self.counter += 1 - yield from self.wait_for_condition(lambda: False, timeout=0.05) - - behaviour = MyAsyncBehaviour() - assert behaviour.counter == 0 - behaviour.act() - assert behaviour.counter == 1 - assert behaviour.state == AsyncBehaviour.AsyncState.RUNNING - - # sleep so the timeout expires - time.sleep(0.1) - - # the next call to act raises TimeoutException - with pytest.raises(TimeoutException): - behaviour.act() - - -def test_async_behaviour_sleep() -> None: - """Test 'sleep' method.""" - - timedelta = 0.5 - - class MyAsyncBehaviour(AsyncBehaviourTest): - counter = 0 - first_datetime = None - last_datetime = None - - def async_act_wrapper(self) -> Generator: - yield from self.async_act() - - def async_act(self) -> Generator: - self.first_datetime = datetime.now() - self.counter += 1 - yield from self.sleep(timedelta) - self.counter += 1 - self.last_datetime = datetime.now() - - behaviour = MyAsyncBehaviour() - assert behaviour.counter == 0 - - behaviour.act() - assert behaviour.counter == 1 - assert behaviour.state == AsyncBehaviour.AsyncState.RUNNING - - # calling 'act()' before the sleep interval will keep the behaviour in the same state - behaviour.act() - assert behaviour.counter == 1 - assert behaviour.state == AsyncBehaviour.AsyncState.RUNNING - - # wait the sleep timeout, we give twice the amount of time it takes the behaviour - time.sleep(timedelta * 2) - - assert behaviour.counter == 1 - assert behaviour.state == AsyncBehaviour.AsyncState.RUNNING - behaviour.act() - assert behaviour.counter == 2 - assert behaviour.state == AsyncBehaviour.AsyncState.READY - assert behaviour.last_datetime is not None and behaviour.first_datetime is not None - assert ( - behaviour.last_datetime - behaviour.first_datetime - ).total_seconds() > timedelta - - -def test_async_behaviour_without_yield() -> None: - """Test AsyncBehaviour, async_act without yield/yield from.""" - - class MyAsyncBehaviour(AsyncBehaviourTest): - def async_act_wrapper(self) -> Generator: - return None # type: ignore # need to check design, not sure it's proper case with return None - - behaviour = MyAsyncBehaviour() - behaviour.act() - assert behaviour.state == AsyncBehaviour.AsyncState.READY - - -def test_async_behaviour_raise_stopiteration() -> None: - """Test AsyncBehaviour, async_act raising 'StopIteration'.""" - - class MyAsyncBehaviour(AsyncBehaviourTest): - def async_act_wrapper(self) -> Generator: - raise StopIteration - - behaviour = MyAsyncBehaviour() - behaviour.act() - assert behaviour.state == AsyncBehaviour.AsyncState.READY - - -def test_async_behaviour_stop() -> None: - """Test AsyncBehaviour.stop method.""" - - class MyAsyncBehaviour(AsyncBehaviourTest): - def async_act(self) -> Generator: - yield - - behaviour = MyAsyncBehaviour() - assert behaviour.is_stopped - behaviour.act() - assert not behaviour.is_stopped - behaviour.stop() - assert behaviour.is_stopped - behaviour.stop() - assert behaviour.is_stopped - - -class RoundA(AbstractRound): - """Concrete ABCI round.""" - - round_id = "round_a" - synchronized_data_class = BaseSynchronizedData - payload_class = MagicMock() - payload_attribute = MagicMock() - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: - """Handle end block.""" - - def check_payload(self, payload: BaseTxPayload) -> None: - """Check payload.""" - - def process_payload(self, payload: BaseTxPayload) -> None: - """Process payload.""" - - -class BehaviourATest(BaseBehaviour): - """Concrete BaseBehaviour class.""" - - matching_round: Type[RoundA] = RoundA - - def async_act(self) -> Generator: - """Do the 'async_act'.""" - yield None - - -def _get_status_patch_wrapper( - latest_block_height: int, - app_hash: str, -) -> Callable[[Any, Any], Generator[None, None, MagicMock]]: - """Wrapper for `_get_status` method patch.""" - - def _get_status_patch(*_: Any, **__: Any) -> Generator[None, None, MagicMock]: - """Patch `_get_status` method""" - yield - return MagicMock( - body=json.dumps( - { - "result": { - "sync_info": { - "latest_block_height": latest_block_height, - "latest_app_hash": app_hash, - } - } - } - ).encode() - ) - - return _get_status_patch - - -def _get_status_wrong_patch( - *args: Any, **kwargs: Any -) -> Generator[None, None, MagicMock]: - """Patch `_get_status` method""" - return MagicMock( - body=json.dumps({"result": {"sync_info": {"latest_block_height": -1}}}).encode() - ) - yield - - -def _wait_until_transaction_delivered_patch( - *args: Any, **kwargs: Any -) -> Generator[None, None, Tuple]: - """Patch `_wait_until_transaction_delivered` method""" - return False, HttpMessage( - performative=HttpMessage.Performative.RESPONSE, # type: ignore - body=json.dumps({"tx_result": {"info": "TransactionNotValidError"}}), - ) - yield - - -def dummy_generator_wrapper(return_value: Any = None) -> Callable[[Any], Generator]: - """A wrapper around a dummy generator that yields nothing and returns the given return value.""" - - def dummy_generator(*_: Any, **__: Any) -> Generator[None, None, Any]: - """A dummy generator that yields nothing and returns the given return value.""" - yield - return return_value - - return dummy_generator - - -class TestBaseBehaviour: - """Tests for the 'BaseBehaviour' class.""" - - _DUMMY_CONSENSUS_THRESHOLD = 3 - - def setup(self) -> None: - """Set up the tests.""" - self.context_mock = MagicMock() - self.context_params_mock = MagicMock( - request_timeout=_DEFAULT_REQUEST_TIMEOUT, - request_retry_delay=_DEFAULT_REQUEST_RETRY_DELAY, - tx_timeout=_DEFAULT_TX_TIMEOUT, - max_attempts=_DEFAULT_TX_MAX_ATTEMPTS, - ) - self.context_mock.shared_state = {} - self.context_state_synchronized_data_mock = MagicMock() - self.context_mock.params = self.context_params_mock - self.context_mock.state.synchronized_data = ( - self.context_state_synchronized_data_mock - ) - self.current_round_count = 10 - self.current_reset_index = 10 - self.context_mock.state.synchronized_data.db = MagicMock( - round_count=self.current_round_count, reset_index=self.current_reset_index - ) - self.context_mock.state.round_sequence.current_round_id = "round_a" - self.context_mock.state.round_sequence.syncing_up = False - self.context_mock.state.round_sequence.block_stall_deadline_expired = False - self.context_mock.http_dialogues = HttpDialogues() - self.context_mock.ipfs_dialogues = IpfsDialogues( - connection_id=str(IPFS_CONNECTION_ID) - ) - self.context_mock.outbox = MagicMock(put_message=self.dummy_put_message) - self.context_mock.requests = MagicMock(request_id_to_callback={}) - self.context_mock.handlers.__dict__ = {"http": MagicMock()} - self.behaviour = BehaviourATest(name="", skill_context=self.context_mock) - self.behaviour.context.logger = logging # type: ignore - self.behaviour.params.sleep_time = 0.01 # type: ignore - - def dummy_put_message(self, *args: Any, **kwargs: Any) -> None: - """A dummy implementation of Outbox.put_message""" - return - - def test_behaviour_id(self) -> None: - """Test behaviour_id on instance.""" - assert self.behaviour.behaviour_id == BehaviourATest.auto_behaviour_id() - - @pytest.mark.parametrize( - "ipfs_response, expected_log", - [ - ( - MagicMock( - ipfs_hash="test", performative=IpfsMessage.Performative.IPFS_HASH - ), - "Successfully stored dummy_filename to IPFS with hash: test", - ), - ( - MagicMock( - ipfs_hash="test", performative=IpfsMessage.Performative.ERROR - ), - f"Expected performative {IpfsMessage.Performative.IPFS_HASH} but got {IpfsMessage.Performative.ERROR}.", - ), - ], - ) - def test_send_to_ipfs( - self, - caplog: LogCaptureFixture, - ipfs_response: IpfsMessage, - expected_log: str, - ) -> None: - """Test send_to_ipfs""" - - def dummy_do_ipfs_req( - *args: Any, **kwargs: Any - ) -> Generator[None, None, Optional[IpfsMessage]]: - """A dummy method to be used in mocks.""" - return ipfs_response - yield - - with mock.patch.object( - IPFSBehaviour, - "_build_ipfs_store_file_req", - return_value=(MagicMock(), MagicMock()), - ) as build_req, mock.patch.object( - BaseBehaviour, "_do_ipfs_request", side_effect=dummy_do_ipfs_req - ) as do_req: - generator = self.behaviour.send_to_ipfs("dummy_filename", {}) - try_send(generator) - build_req.assert_called() - do_req.assert_called() - assert expected_log in caplog.text - - def test_ipfs_store_fails(self, caplog: LogCaptureFixture) -> None: - """Test for failure during building store_file_req.""" - expected_logs = "An error occurred while trying to send a file to IPFS:" - with mock.patch.object( - IPFSBehaviour, - "_build_ipfs_store_file_req", - side_effect=IPFSInteractionError, - ), caplog.at_level(logging.ERROR): - generator = self.behaviour.send_to_ipfs("dummy_filename", {}) - try_send(generator) - assert expected_logs in caplog.text - - def test_do_ipfs_request(self) -> None: - """Test _do_ipfs_request""" - message, dialogue = cast( - IpfsDialogues, self.context_mock.ipfs_dialogues - ).create(str(IPFS_CONNECTION_ID), IpfsMessage.Performative.GET_FILES) - message = cast(IpfsMessage, message) - dialogue = cast(IpfsDialogue, dialogue) - - def dummy_wait_for_message( - *args: Any, **kwargs: Any - ) -> Generator[None, None, Message]: - """A dummy implementation of AsyncBehaviour.wait_for_message to be used for mocks.""" - return MagicMock() - yield - - with mock.patch.object( - AsyncBehaviour, "wait_for_message", side_effect=dummy_wait_for_message - ): - gen = self.behaviour._do_ipfs_request( - dialogue, - message, - ) - try_send(gen) - - @pytest.mark.parametrize( - "ipfs_response, expected_log", - [ - ( - MagicMock( - files={"dummy_file_name": "test"}, - performative=IpfsMessage.Performative.FILES, - ), - "Retrieved 1 objects from ipfs.", - ), - ( - MagicMock( - ipfs_hash="test", performative=IpfsMessage.Performative.ERROR - ), - f"Expected performative {IpfsMessage.Performative.FILES} but got {IpfsMessage.Performative.ERROR}.", - ), - ], - ) - def test_get_from_ipfs( - self, - caplog: LogCaptureFixture, - ipfs_response: IpfsMessage, - expected_log: str, - ) -> None: - """Test get_from_ipfs""" - - def dummy_do_ipfs_req( - *args: Any, **kwargs: Any - ) -> Generator[None, None, Optional[IpfsMessage]]: - """A dummy method to be used in mocks.""" - return ipfs_response - yield - - with mock.patch.object( - IPFSBehaviour, - "_build_ipfs_get_file_req", - return_value=(MagicMock(), MagicMock()), - ) as build_req, mock.patch.object( - IPFSBehaviour, - "_deserialize_ipfs_objects", - return_value=MagicMock(), - ), mock.patch.object( - BaseBehaviour, "_do_ipfs_request", side_effect=dummy_do_ipfs_req - ) as do_req: - generator = self.behaviour.get_from_ipfs("dummy_ipfs_hash") - try_send(generator) - build_req.assert_called() - do_req.assert_called() - assert expected_log in caplog.text - - def test_ipfs_get_fails(self, caplog: LogCaptureFixture) -> None: - """Test for failure during building get_files req.""" - expected_logs = "An error occurred while trying to fetch a file from IPFS:" - with mock.patch.object( - IPFSBehaviour, "_build_ipfs_get_file_req", side_effect=IPFSInteractionError - ), caplog.at_level(logging.ERROR): - generator = self.behaviour.get_from_ipfs("dummy_ipfs_hash") - try_send(generator) - assert expected_logs in caplog.text - - def test_params_property(self) -> None: - """Test the 'params' property.""" - assert self.behaviour.params == self.context_params_mock - - def test_synchronized_data_property(self) -> None: - """Test the 'synchronized_data' property.""" - assert ( - self.behaviour.synchronized_data - == self.context_state_synchronized_data_mock - ) - - def test_check_in_round(self) -> None: - """Test 'BaseBehaviour' initialization.""" - expected_round_id = "round" - self.context_mock.state.round_sequence.current_round_id = expected_round_id - assert self.behaviour.check_in_round(expected_round_id) - assert not self.behaviour.check_in_round("wrong round") - - assert not self.behaviour.check_not_in_round(expected_round_id) - assert self.behaviour.check_not_in_round("wrong round") - - func = self.behaviour.is_round_ended(expected_round_id) - assert not func() - - def test_check_in_last_round(self) -> None: - """Test 'BaseBehaviour' initialization.""" - expected_round_id = "round" - self.context_mock.state.round_sequence.last_round_id = expected_round_id - assert self.behaviour.check_in_last_round(expected_round_id) - assert not self.behaviour.check_in_last_round("wrong round") - - assert not self.behaviour.check_not_in_last_round(expected_round_id) - assert self.behaviour.check_not_in_last_round("wrong round") - - assert self.behaviour.check_round_has_finished(expected_round_id) - - def test_check_round_height_has_changed(self) -> None: - """Test 'check_round_height_has_changed'.""" - current_height = 0 - self.context_mock.state.round_sequence.current_round_height = current_height - assert not self.behaviour.check_round_height_has_changed(current_height) - new_height = current_height + 1 - self.context_mock.state.round_sequence.current_round_height = new_height - assert self.behaviour.check_round_height_has_changed(current_height) - assert not self.behaviour.check_round_height_has_changed(new_height) - - def test_wait_until_round_end_negative_last_round_or_matching_round(self) -> None: - """Test 'wait_until_round_end' method, negative case (not in matching nor last round).""" - self.behaviour.context.state.round_sequence.current_round_id = ( - "current_round_id" - ) - self.behaviour.context.state.round_sequence.last_round_id = "last_round_id" - self.behaviour.matching_round.round_id = "matching_round" - generator = self.behaviour.wait_until_round_end() - with pytest.raises( - ValueError, - match=r"Should be in matching round \(matching_round\) or last round \(last_round_id\), actual round current_round_id!", - ): - generator.send(None) - - @mock.patch.object(BaseBehaviour, "wait_for_condition") - @mock.patch.object(BaseBehaviour, "check_not_in_round", return_value=False) - @mock.patch.object(BaseBehaviour, "check_not_in_last_round", return_value=False) - def test_wait_until_round_end_positive(self, *_: Any) -> None: - """Test 'wait_until_round_end' method, positive case.""" - gen = self.behaviour.wait_until_round_end() - try_send(gen) - - def test_wait_from_last_timestamp(self) -> None: - """Test 'wait_from_last_timestamp'.""" - timeout = 1.0 - last_timestamp = datetime.now() - self.behaviour.context.state.round_sequence.abci_app.last_timestamp = ( - last_timestamp - ) - gen = self.behaviour.wait_from_last_timestamp(timeout) - # trigger first execution - try_send(gen) - # at the time this line is executed, the generator is not empty - # as the timeout has not run out yet - try_send(gen) - # sleep enough time to make the timeout to run out - time.sleep(timeout) - # the next iteration of the generator raises StopIteration - # because its execution terminates - with pytest.raises(StopIteration): - gen.send(MagicMock()) - - def test_wait_from_last_timestamp_negative(self) -> None: - """Test 'wait_from_last_timestamp'.""" - timeout = -1.0 - last_timestamp = datetime.now() - self.behaviour.context.state.round_sequence.abci_app.last_timestamp = ( - last_timestamp - ) - with pytest.raises(ValueError): - gen = self.behaviour.wait_from_last_timestamp(timeout) - # trigger first execution - try_send(gen) - - def test_set_done(self) -> None: - """Test 'set_done' method.""" - assert not self.behaviour.is_done() - self.behaviour.set_done() - assert self.behaviour.is_done() - - @mock.patch.object(BaseBehaviour, "_send_transaction") - def test_send_a2a_transaction_positive(self, *_: Any) -> None: - """Test 'send_a2a_transaction' method, positive case.""" - gen = self.behaviour.send_a2a_transaction(MagicMock()) - try_send(gen) - - def test_async_act_wrapper_agent_sync_mode( - self, - ) -> None: - """Test 'async_act_wrapper' in sync mode.""" - self.behaviour.context.state.round_sequence.syncing_up = True - self.behaviour.context.state.round_sequence.height = 0 - self.behaviour.matching_round = MagicMock() - - with mock.patch.object(logging, "info") as log_mock, mock.patch.object( - BaseBehaviour, - "_get_status", - _get_status_patch_wrapper(0, "test"), - ): - gen = self.behaviour.async_act_wrapper() - for __ in range(3): - try_send(gen) - log_mock.assert_called_with( - "local height == remote == 0; Synchronization complete." - ) - - @mock.patch.object(BaseBehaviour, "_get_status", _get_status_wrong_patch) - def test_async_act_wrapper_agent_sync_mode_where_height_dont_match(self) -> None: - """Test 'async_act_wrapper' in sync mode.""" - self.behaviour.context.state.round_sequence.syncing_up = True - self.behaviour.context.state.round_sequence.height = 0 - self.behaviour.context.params.tendermint_check_sleep_delay = 3 - self.behaviour.matching_round = MagicMock() - - gen = self.behaviour.async_act_wrapper() - try_send(gen) - - @pytest.mark.parametrize("exception_cls", [StopIteration]) - def test_async_act_wrapper_exception(self, exception_cls: Exception) -> None: - """Test 'async_act_wrapper'.""" - with mock.patch.object(self.behaviour, "async_act", side_effect=exception_cls): - with mock.patch.object(self.behaviour, "clean_up") as clean_up_mock: - gen = self.behaviour.async_act_wrapper() - try_send(gen) - try_send(gen) - clean_up_mock.assert_called() - - def test_get_request_nonce_from_dialogue(self) -> None: - """Test '_get_request_nonce_from_dialogue' helper method.""" - dialogue_mock = MagicMock() - expected_value = "dialogue_reference" - dialogue_mock.dialogue_label.dialogue_reference = (expected_value, None) - result = BaseBehaviour._get_request_nonce_from_dialogue(dialogue_mock) - assert result == expected_value - - @mock.patch.object(BaseBehaviour, "_send_signing_request") - @mock.patch.object(Transaction, "encode", return_value=MagicMock()) - @mock.patch.object( - BaseBehaviour, - "_build_http_request_message", - return_value=(MagicMock(), MagicMock()), - ) - @mock.patch.object(BaseBehaviour, "_check_http_return_code_200", return_value=False) - def test_send_transaction_stop_condition(self, *_: Any) -> None: - """Test '_send_transaction' method's `stop_condition` as provided by `send_a2a_transaction`.""" - request_retry_delay = 0.01 - # set the current round's id so that it does not meet the requirements for a `stop_condition` - self.behaviour.context.state.round_sequence.current_round_id = ( - self.behaviour.matching_round.round_id - ) = "round_a" - # assert that everything is pre-set correctly - assert ( - self.behaviour.context.state.round_sequence.current_round_id - == self.behaviour.matching_round.auto_round_id() - == "round_a" - ) - - # create the exact same stop condition that we create in the `send_a2a_transaction` method - stop_condition = self.behaviour.is_round_ended( - self.behaviour.matching_round.auto_round_id() - ) - gen = self.behaviour._send_transaction( - MagicMock(), - request_retry_delay=request_retry_delay, - stop_condition=stop_condition, - ) - # assert that the stop condition does not apply yet - assert not stop_condition() - # trigger the generator function so that we enter the `stop_condition` loop - try_send(gen) - - # set the current round's id so that it meets the requirements for a `stop_condition` - self.behaviour.context.state.round_sequence.current_round_id = "test" - - # assert that everything was set as expected - assert ( - self.behaviour.context.state.round_sequence.current_round_id - != self.behaviour.matching_round.auto_round_id() - and self.behaviour.context.state.round_sequence.current_round_id == "test" - ) - # assert that the stop condition now applies - assert stop_condition() - - # test with a non-200 response in order to cause the execution to re-enter the while `stop_condition` - # we expect that the second time we will not enter, since we have caused the `stop_condition` to be `True` - with mock.patch.object( - self.behaviour.context.logger, "debug" - ) as mock_debug, mock.patch.object( - self.behaviour.context.logger, "error" - ) as mock_error: - # send message to 'wait_for_message' - try_send(gen, obj=MagicMock(status_code=200)) - # send message to '_submit_tx' - response = MagicMock(body='{"result": {"hash": "", "code": 0}}') - try_send(gen, obj=response) - mock_error.assert_called_with( - f"Received return code != 200 with response {response} with body {str(response.body)}. " - f"Retrying in {request_retry_delay} seconds..." - ) - time.sleep(request_retry_delay) - try_send(gen) - # assert that the stop condition is now `True` and we reach at the end of the method - mock_debug.assert_called_with( - "Stop condition is true, no more attempts to send the transaction." - ) - - def test_send_transaction_positive_false_condition(self) -> None: - """Test '_send_transaction', positive case (false condition)""" - with mock.patch.object(self.behaviour.context.logger, "debug") as mock_debug: - try_send( - self.behaviour._send_transaction( - MagicMock(), stop_condition=lambda: True - ) - ) - mock_debug.assert_called_with( - "Stop condition is true, no more attempts to send the transaction." - ) - - @mock.patch.object(BaseBehaviour, "_send_signing_request") - @mock.patch.object(Transaction, "encode", return_value=MagicMock()) - @mock.patch.object( - BaseBehaviour, - "_build_http_request_message", - return_value=(MagicMock(), MagicMock()), - ) - @mock.patch.object(BaseBehaviour, "_check_http_return_code_200", return_value=True) - def test_send_transaction_positive(self, *_: Any) -> None: - """Test '_send_transaction', positive case.""" - m = MagicMock(status_code=200) - gen = self.behaviour._send_transaction(m) - # trigger generator function - try_send(gen, obj=None) - # send message to 'wait_for_message' - try_send(gen, obj=m) - # send message to '_submit_tx' - try_send(gen, obj=MagicMock(body='{"result": {"hash": "", "code": 0}}')) - # send message to '_wait_until_transaction_delivered' - success_response = MagicMock( - status_code=200, body='{"result": {"tx_result": {"code": 0}}}' - ) - try_send(gen, obj=success_response) - - @mock.patch.object(BaseBehaviour, "_send_signing_request") - @mock.patch.object(Transaction, "encode", return_value=MagicMock()) - @mock.patch.object( - BaseBehaviour, - "_build_http_request_message", - return_value=(MagicMock(), MagicMock()), - ) - @mock.patch.object(BaseBehaviour, "_check_http_return_code_200", return_value=True) - @mock.patch.object( - BaseBehaviour, - "_wait_until_transaction_delivered", - new=_wait_until_transaction_delivered_patch, - ) - def test_send_transaction_invalid_transaction(self, *_: Any) -> None: - """Test '_send_transaction', positive case.""" - m = MagicMock(status_code=200) - gen = self.behaviour._send_transaction(m) - try_send(gen, obj=None) - try_send(gen, obj=m) - try_send(gen, obj=MagicMock(body='{"result": {"hash": "", "code": 0}}')) - success_response = MagicMock( - status_code=200, body='{"result": {"tx_result": {"code": 0}}}' - ) - try_send(gen, obj=success_response) - - @mock.patch.object(BaseBehaviour, "_send_signing_request") - @mock.patch.object(BaseBehaviour, "_is_invalid_transaction", return_value=False) - @mock.patch.object(BaseBehaviour, "_tx_not_found", return_value=True) - @mock.patch.object(Transaction, "encode", return_value=MagicMock()) - @mock.patch.object( - BaseBehaviour, - "_build_http_request_message", - return_value=(MagicMock(), MagicMock()), - ) - @mock.patch.object(BaseBehaviour, "_check_http_return_code_200", return_value=True) - @mock.patch.object( - BaseBehaviour, - "_wait_until_transaction_delivered", - new=_wait_until_transaction_delivered_patch, - ) - def test_send_transaction_valid_transaction(self, *_: Any) -> None: - """Test '_send_transaction', positive case.""" - m = MagicMock(status_code=200) - gen = self.behaviour._send_transaction(m) - try_send(gen, obj=None) - try_send(gen, obj=m) - try_send(gen, obj=MagicMock(body='{"result": {"hash": "", "code": 0}}')) - success_response = MagicMock( - status_code=200, body='{"result": {"tx_result": {"code": 0}}}' - ) - try_send(gen, obj=success_response) - - def test_tx_not_found(self, *_: Any) -> None: - """Test _tx_not_found""" - res = MagicMock( - body='{"error": {"code": "dummy_code", "message": "dummy_message", "data": "dummy_data"}}' - ) - self.behaviour._tx_not_found(tx_hash="tx_hash", res=res) - - @mock.patch.object(BaseBehaviour, "_send_signing_request") - def test_send_transaction_signing_error(self, *_: Any) -> None: - """Test '_send_transaction', signing error.""" - m = MagicMock(performative=SigningMessage.Performative.ERROR) - gen = self.behaviour._send_transaction(m) - # trigger generator function - try_send(gen, obj=None) - with pytest.raises(RuntimeError): - try_send(gen, obj=m) - - @mock.patch.object(BaseBehaviour, "_send_signing_request") - @mock.patch.object(Transaction, "encode", return_value=MagicMock()) - @mock.patch.object( - BaseBehaviour, - "_build_http_request_message", - return_value=(MagicMock(), MagicMock()), - ) - def test_send_transaction_timeout_exception_submit_tx(self, *_: Any) -> None: - """Test '_send_transaction', timeout exception.""" - timeout = 0.05 - delay = 0.1 - m = MagicMock() - with mock.patch.object( - self.behaviour.context.logger, "warning" - ) as mock_warning: - gen = self.behaviour._send_transaction( - m, request_timeout=timeout, request_retry_delay=delay - ) - # trigger generator function - try_send(gen, obj=None) - try_send(gen, obj=m) - time.sleep(timeout) - try_send(gen, obj=m) - time.sleep(delay) - mock_warning.assert_called_with( - f"Timeout expired for submit tx. Retrying in {delay} seconds..." - ) - try_send(gen, obj=None) - - @mock.patch.object(BaseBehaviour, "_send_signing_request") - @mock.patch.object(Transaction, "encode", return_value=MagicMock()) - @mock.patch.object( - BaseBehaviour, - "_build_http_request_message", - return_value=(MagicMock(), MagicMock()), - ) - @mock.patch.object(BaseBehaviour, "_check_http_return_code_200", return_value=True) - def test_send_transaction_timeout_exception_wait_until_transaction_delivered( - self, *_: Any - ) -> None: - """Test '_send_transaction', timeout exception.""" - timeout = 0.05 - delay = 0.1 - m = MagicMock() - with mock.patch.object( - self.behaviour.context.logger, "warning" - ) as mock_warning: - gen = self.behaviour._send_transaction( - m, request_retry_delay=delay, tx_timeout=timeout - ) - # trigger generator function - try_send(gen, obj=None) - # send message to 'wait_for_message' - try_send(gen, obj=m) - # send message to '_submit_tx' - try_send(gen, obj=MagicMock(body='{"result": {"hash": "", "code": 0}}')) - # send message to '_wait_until_transaction_delivered' - time.sleep(timeout) - try_send(gen, obj=m) - - mock_warning.assert_called_with( - f"Timeout expired for wait until transaction delivered. Retrying in {delay} seconds..." - ) - - @mock.patch.object(BaseBehaviour, "_send_signing_request") - @mock.patch.object(Transaction, "encode", return_value=MagicMock()) - @mock.patch.object( - BaseBehaviour, - "_build_http_request_message", - return_value=(MagicMock(), MagicMock()), - ) - @mock.patch.object(BaseBehaviour, "_check_http_return_code_200", return_value=True) - def test_send_transaction_transaction_not_delivered(self, *_: Any) -> None: - """Test '_send_transaction', timeout exception.""" - timeout = 0.05 - delay = 0.1 - m = MagicMock() - with mock.patch.object( - self.behaviour.context.logger, "warning" - ) as mock_warning: - gen = self.behaviour._send_transaction( - m, request_retry_delay=delay, tx_timeout=timeout, max_attempts=0 - ) - # trigger generator function - try_send(gen, obj=None) - # send message to 'wait_for_message' - try_send(gen, obj=m) - # send message to '_submit_tx' - try_send(gen, obj=MagicMock(body='{"result": {"hash": "", "code": 0}}')) - # send message to '_wait_until_transaction_delivered' - time.sleep(timeout) - try_send(gen, obj=m) - - mock_warning.assert_called_with( - "Tx sent but not delivered. Response = None" - ) - - @mock.patch.object(BaseBehaviour, "_send_signing_request") - @mock.patch.object(Transaction, "encode", return_value=MagicMock()) - @mock.patch.object( - BaseBehaviour, - "_build_http_request_message", - return_value=(MagicMock(), MagicMock()), - ) - @mock.patch.object(BaseBehaviour, "_check_http_return_code_200", return_value=True) - def test_send_transaction_wrong_ok_code(self, *_: Any) -> None: - """Test '_send_transaction', positive case.""" - m = MagicMock(status_code=200) - gen = self.behaviour._send_transaction(m) - - with mock.patch.object(self.behaviour.context.logger, "error") as mock_error: - # trigger generator function - try_send(gen, obj=None) - # send message to 'wait_for_message' - try_send(gen, obj=m) - # send message to '_submit_tx' - try_send(gen, obj=MagicMock(body='{"result": {"hash": "", "code": -1}}')) - # send message to '_wait_until_transaction_delivered' - success_response = MagicMock( - status_code=200, body='{"result": {"tx_result": {"code": 0}}}' - ) - try_send(gen, obj=success_response) - - mock_error.assert_called_with( - "Received tendermint code != 0. Retrying in 1.0 seconds..." - ) - - @mock.patch.object(BaseBehaviour, "_send_signing_request") - @mock.patch.object(Transaction, "encode", return_value=MagicMock()) - @mock.patch.object( - BaseBehaviour, - "_build_http_request_message", - return_value=(MagicMock(), MagicMock()), - ) - @mock.patch.object( - BaseBehaviour, - "_check_http_return_code_200", - return_value=True, - ) - @mock.patch("json.loads", return_value={"result": {"hash": "", "code": OK_CODE}}) - def test_send_transaction_wait_delivery_timeout_exception(self, *_: Any) -> None: - """Test '_send_transaction', timeout exception on tx delivery.""" - timeout = 0.05 - delay = 0.1 - m = MagicMock() - with mock.patch.object( - self.behaviour.context.logger, "warning" - ) as mock_warning: - gen = self.behaviour._send_transaction( - m, - request_timeout=timeout, - request_retry_delay=delay, - tx_timeout=timeout, - ) - # trigger generator function - try_send(gen, obj=None) - try_send(gen, obj=m) - try_send(gen, obj=m) - time.sleep(timeout) - try_send(gen, obj=m) - mock_warning.assert_called_with( - f"Timeout expired for wait until transaction delivered. Retrying in {delay} seconds..." - ) - time.sleep(delay) - try_send(gen, obj=m) - - @pytest.mark.parametrize("resetting", (True, False)) - @pytest.mark.parametrize( - "non_200_count", - ( - 0, - NON_200_RETURN_CODE_DURING_RESET_THRESHOLD, - NON_200_RETURN_CODE_DURING_RESET_THRESHOLD + 1, - ), - ) - @mock.patch.object(BaseBehaviour, "_send_signing_request") - @mock.patch.object(Transaction, "encode", return_value=MagicMock()) - @mock.patch.object( - BaseBehaviour, - "_build_http_request_message", - return_value=(MagicMock(), MagicMock()), - ) - @mock.patch("json.loads") - def test_send_transaction_error_status_code( - self, _: Any, __: Any, ___: Any, ____: Any, resetting: bool, non_200_count: int - ) -> None: - """Test '_send_transaction', error status code.""" - delay = 0.1 - self.behaviour._non_200_return_code_count = non_200_count - m = MagicMock() - with mock.patch.object(self.behaviour.context.logger, "error") as mock_error: - gen = self.behaviour._send_transaction( - m, resetting, request_retry_delay=delay - ) - # trigger generator function - try_send(gen, obj=None) - try_send(gen, obj=m) - # send message to '_submit_tx' - res = MagicMock(body="{'test': 'test'}") - try_send(gen, obj=res) - if ( - resetting - and non_200_count <= NON_200_RETURN_CODE_DURING_RESET_THRESHOLD - ): - mock_error.assert_not_called() - else: - mock_error.assert_called_with( - f"Received return code != 200 with response {res} with body {str(res.body)}. " - f"Retrying in {delay} seconds..." - ) - time.sleep(delay) - try_send(gen, obj=None) - - @mock.patch.object(BaseBehaviour, "_get_request_nonce_from_dialogue") - @mock.patch.object(behaviour_utils, "RawMessage") - @mock.patch.object(behaviour_utils, "Terms") - def test_send_signing_request(self, *_: Any) -> None: - """Test '_send_signing_request'.""" - with mock.patch.object( - self.behaviour.context.signing_dialogues, - "create", - return_value=(MagicMock(), MagicMock()), - ): - self.behaviour._send_signing_request(b"") - - @given(st.binary()) - def test_fuzz_send_signing_request(self, input_bytes: bytes) -> None: - """Fuzz '_send_signing_request'. - - Mock context manager decorators don't work here. - - :param input_bytes: fuzz input - """ - with mock.patch.object( - self.behaviour.context.signing_dialogues, - "create", - return_value=(MagicMock(), MagicMock()), - ): - with mock.patch.object(behaviour_utils, "RawMessage"): - with mock.patch.object(behaviour_utils, "Terms"): - self.behaviour._send_signing_request(input_bytes) - - @mock.patch.object(BaseBehaviour, "_get_request_nonce_from_dialogue") - @mock.patch.object(behaviour_utils, "RawMessage") - @mock.patch.object(behaviour_utils, "Terms") - def test_send_transaction_signing_request(self, *_: Any) -> None: - """Test '_send_signing_request'.""" - with mock.patch.object( - self.behaviour.context.signing_dialogues, - "create", - return_value=(MagicMock(), MagicMock()), - ): - self.behaviour._send_transaction_signing_request(MagicMock(), MagicMock()) - - @pytest.mark.parametrize( - "use_flashbots, target_block_numbers, expected_kwargs", - ( - ( - True, - None, - dict( - counterparty=LEDGER_API_ADDRESS, - performative=LedgerApiMessage.Performative.SEND_SIGNED_TRANSACTIONS, - signed_transactions=SignedTransactions( - ledger_id="ethereum_flashbots", - signed_transactions=[{"test_tx": "test_tx"}], - ), - kwargs=LedgerApiMessage.Kwargs( - { - "chain_id": None, - "raise_on_failed_simulation": False, - "use_all_builders": True, - } - ), - ), - ), - ( - True, - [1, 2, 3], - dict( - counterparty=LEDGER_API_ADDRESS, - performative=LedgerApiMessage.Performative.SEND_SIGNED_TRANSACTIONS, - signed_transactions=SignedTransactions( - ledger_id="ethereum_flashbots", - signed_transactions=[{"test_tx": "test_tx"}], - ), - kwargs=LedgerApiMessage.Kwargs( - { - "chain_id": None, - "raise_on_failed_simulation": False, - "use_all_builders": True, - "target_block_numbers": [1, 2, 3], - } - ), - ), - ), - ( - False, - None, - dict( - counterparty=LEDGER_API_ADDRESS, - performative=LedgerApiMessage.Performative.SEND_SIGNED_TRANSACTION, - signed_transaction=SignedTransaction( - ledger_id="ethereum", body={"test_tx": "test_tx"} - ), - ), - ), - ), - ) - def test_send_transaction_request( - self, - use_flashbots: bool, - target_block_numbers: Optional[List[int]], - expected_kwargs: Any, - ) -> None: - """Test '_send_transaction_request'.""" - with mock.patch.object( - self.behaviour.context.ledger_api_dialogues, - "create", - return_value=(MagicMock(), MagicMock()), - ) as create_mock: - self.behaviour._send_transaction_request( - MagicMock( - signed_transaction=SignedTransaction( - ledger_id="ethereum", body={"test_tx": "test_tx"} - ) - ), - use_flashbots, - target_block_numbers, - ) - create_mock.assert_called_once() - # not using `create_mock.call_args.kwargs` because it is not compatible with Python 3.7 - actual_kwargs = create_mock.call_args[1] - assert actual_kwargs == expected_kwargs - - def test_send_transaction_receipt_request(self) -> None: - """Test '_send_transaction_receipt_request'.""" - with mock.patch.object( - self.behaviour.context.ledger_api_dialogues, - "create", - return_value=(MagicMock(), MagicMock()), - ): - self.behaviour.context.default_ledger_id = "default_ledger_id" - self.behaviour._send_transaction_receipt_request("digest") - - def test_build_http_request_message(self, *_: Any) -> None: - """Test '_build_http_request_message'.""" - with mock.patch.object( - self.behaviour.context.http_dialogues, - "create", - return_value=(MagicMock(), MagicMock()), - ): - self.behaviour._build_http_request_message( - "", - "", - parameters={"foo": "bar"}, - headers={"foo": "foo_val", "bar": "bar_val"}, - ) - - @mock.patch.object(Transaction, "encode", return_value=MagicMock()) - @mock.patch.object( - BaseBehaviour, - "_build_http_request_message", - return_value=(MagicMock(), MagicMock()), - ) - @mock.patch.object(BaseBehaviour, "_check_http_return_code_200", return_value=True) - @mock.patch.object(BaseBehaviour, "sleep") - @mock.patch("json.loads") - def test_wait_until_transaction_delivered(self, *_: Any) -> None: - """Test '_wait_until_transaction_delivered' method.""" - gen = self.behaviour._wait_until_transaction_delivered(MagicMock()) - # trigger generator function - try_send(gen, obj=None) - - # first check attempt fails - failure_response = MagicMock(status_code=500) - try_send(gen, failure_response) - - # second check attempt succeeds - success_response = MagicMock( - status_code=200, body='{"result": {"tx_result": {"code": 0}}}' - ) - try_send(gen, success_response) - - @mock.patch.object(Transaction, "encode", return_value=MagicMock()) - @mock.patch.object( - BaseBehaviour, - "_build_http_request_message", - return_value=(MagicMock(), MagicMock()), - ) - @mock.patch.object(BaseBehaviour, "_check_http_return_code_200", return_value=True) - @mock.patch.object(BaseBehaviour, "sleep") - @mock.patch("json.loads") - def test_wait_until_transaction_delivered_failed(self, *_: Any) -> None: - """Test '_wait_until_transaction_delivered' method.""" - gen = self.behaviour._wait_until_transaction_delivered( - MagicMock(), max_attempts=0 - ) - # trigger generator function - try_send(gen, obj=None) - - # first check attempt fails - failure_response = MagicMock(status_code=500) - try_send(gen, failure_response) - - # second check attempt succeeds - success_response = MagicMock( - status_code=200, body='{"result": {"tx_result": {"code": -1}}}' - ) - try_send(gen, success_response) - - @pytest.mark.skipif( - platform.system() == "Windows", - reason="https://github.com/valory-xyz/open-autonomy/issues/1477", - ) - def test_wait_until_transaction_delivered_raises_timeout(self, *_: Any) -> None: - """Test '_wait_until_transaction_delivered' method.""" - gen = self.behaviour._wait_until_transaction_delivered(MagicMock(), timeout=0.0) - with pytest.raises(TimeoutException): - # trigger generator function - try_send(gen, obj=None) - - @mock.patch.object(behaviour_utils, "Terms") - def test_get_default_terms(self, *_: Any) -> None: - """Test '_get_default_terms'.""" - self.behaviour._get_default_terms() - - @mock.patch.object(BaseBehaviour, "_send_transaction_signing_request") - @mock.patch.object(BaseBehaviour, "_send_transaction_request") - @mock.patch.object(BaseBehaviour, "_send_transaction_receipt_request") - @mock.patch.object(behaviour_utils, "Terms") - @pytest.mark.parametrize( - "ledger_message, expected_hash, expected_response_status", - ( - ( - LedgerApiMessage( - cast( - LedgerApiMessage.Performative, - LedgerApiMessage.Performative.TRANSACTION_DIGEST, - ), - ("", ""), - transaction_digest=TransactionDigest("ledger_id", body="test"), - ), - "test", - RPCResponseStatus.SUCCESS, - ), - ( - LedgerApiMessage( - cast( - LedgerApiMessage.Performative, - LedgerApiMessage.Performative.TRANSACTION_DIGESTS, - ), - ("", ""), - # Only the first hash will be considered - # because we do not support sending multiple messages and receiving multiple tx hashes yet - transaction_digests=TransactionDigests( - "ledger_id", - transaction_digests=["test", "will_not_be_considered"], - ), - ), - "test", - RPCResponseStatus.SUCCESS, - ), - ), - ) - def test_send_raw_transaction( - self, - _send_transaction_signing_request: Any, - _send_transaction_request: Any, - _send_transaction_receipt_request: Any, - _terms: Any, - ledger_message: LedgerApiMessage, - expected_hash: str, - expected_response_status: RPCResponseStatus, - ) -> None: - """Test 'send_raw_transaction'.""" - m = MagicMock() - gen = self.behaviour.send_raw_transaction(m) - # trigger generator function - gen.send(None) - gen.send( - SigningMessage( - cast( - SigningMessage.Performative, - SigningMessage.Performative.SIGNED_TRANSACTION, - ), - ("", ""), - signed_transaction=SignedTransaction( - "ledger_id", body={"hash": expected_hash} - ), - ) - ) - try: - gen.send(ledger_message) - raise ValueError("Generator was expected to have reached its end!") - except StopIteration as e: - tx_hash, status = e.value - - assert tx_hash == expected_hash - assert status == expected_response_status - - @mock.patch.object(BaseBehaviour, "_send_transaction_signing_request") - @mock.patch.object(BaseBehaviour, "_send_transaction_request") - @mock.patch.object(BaseBehaviour, "_send_transaction_receipt_request") - @mock.patch.object(behaviour_utils, "Terms") - def test_send_raw_transaction_with_wrong_signing_performative( - self, *_: Any - ) -> None: - """Test 'send_raw_transaction'.""" - m = MagicMock() - gen = self.behaviour.send_raw_transaction(m) - # trigger generator function - gen.send(None) - try: - gen.send(MagicMock(performative=SigningMessage.Performative.ERROR)) - raise ValueError("Generator was expected to have reached its end!") - except StopIteration as e: - tx_hash, status = e.value - - assert tx_hash is None - assert status == RPCResponseStatus.UNCLASSIFIED_ERROR - - @pytest.mark.parametrize( - "message, expected_rpc_status", - ( - ("Simulation failed for bundle", RPCResponseStatus.SIMULATION_FAILED), - ("replacement transaction underpriced", RPCResponseStatus.UNDERPRICED), - ("nonce too low", RPCResponseStatus.INCORRECT_NONCE), - ("insufficient funds", RPCResponseStatus.INSUFFICIENT_FUNDS), - ("already known", RPCResponseStatus.ALREADY_KNOWN), - ("test", RPCResponseStatus.UNCLASSIFIED_ERROR), - ), - ) - @mock.patch.object(BaseBehaviour, "_send_transaction_signing_request") - @mock.patch.object(BaseBehaviour, "_send_transaction_request") - @mock.patch.object(BaseBehaviour, "_send_transaction_receipt_request") - @mock.patch.object(behaviour_utils, "Terms") - def test_send_raw_transaction_errors( - self, - _: Any, - __: Any, - ___: Any, - ____: Any, - message: str, - expected_rpc_status: RPCResponseStatus, - ) -> None: - """Test 'send_raw_transaction'.""" - m = MagicMock() - gen = self.behaviour.send_raw_transaction(m) - # trigger generator function - gen.send(None) - gen.send( - SigningMessage( - cast( - SigningMessage.Performative, - SigningMessage.Performative.SIGNED_TRANSACTION, - ), - ("", ""), - signed_transaction=SignedTransaction( - "ledger_id", body={"hash": "test"} - ), - ) - ) - try: - gen.send( - LedgerApiMessage( - cast( - LedgerApiMessage.Performative, - LedgerApiMessage.Performative.ERROR, - ), - ("", ""), - message=message, - ) - ) - raise ValueError("Generator was expected to have reached its end!") - except StopIteration as e: - tx_hash, status = e.value - - assert tx_hash == "test" - assert status == expected_rpc_status - - @mock.patch.object(BaseBehaviour, "_send_transaction_signing_request") - @mock.patch.object(BaseBehaviour, "_send_transaction_request") - @mock.patch.object(BaseBehaviour, "_send_transaction_receipt_request") - @mock.patch.object(behaviour_utils, "Terms") - def test_send_raw_transaction_hashes_mismatch(self, *_: Any) -> None: - """Test 'send_raw_transaction' when signature and tx responses' hashes mismatch.""" - m = MagicMock() - gen = self.behaviour.send_raw_transaction(m) - # trigger generator function - gen.send(None) - gen.send( - SigningMessage( - cast( - SigningMessage.Performative, - SigningMessage.Performative.SIGNED_TRANSACTION, - ), - ("", ""), - signed_transaction=SignedTransaction( - "ledger_id", body={"hash": "signed"} - ), - ) - ) - try: - gen.send( - LedgerApiMessage( - cast( - LedgerApiMessage.Performative, - LedgerApiMessage.Performative.TRANSACTION_DIGEST, - ), - ("", ""), - transaction_digest=TransactionDigest("ledger_id", body="tx"), - ) - ) - raise ValueError("Generator was expected to have reached its end!") - except StopIteration as e: - tx_hash, status = e.value - - assert tx_hash is None - assert status == RPCResponseStatus.UNCLASSIFIED_ERROR - - def test_get_transaction_receipt(self, caplog: LogCaptureFixture) -> None: - """Test get_transaction_receipt.""" - - expected: JSONLike = {"dummy": "tx_receipt"} - transaction_receipt = LedgerApiMessage.TransactionReceipt("", expected, {}) - tx_receipt_message = LedgerApiMessage( - LedgerApiMessage.Performative.TRANSACTION_RECEIPT, # type: ignore - transaction_receipt=transaction_receipt, - ) - side_effect = mock_yield_and_return(tx_receipt_message) - with as_context( - mock.patch.object(self.behaviour, "_send_transaction_receipt_request"), - mock.patch.object( - self.behaviour, "wait_for_message", side_effect=side_effect - ), - ): - gen = self.behaviour.get_transaction_receipt("tx_digest") - try: - while True: - next(gen) - except StopIteration as e: - assert e.value == expected - - def test_get_transaction_receipt_error(self, caplog: LogCaptureFixture) -> None: - """Test get_transaction_receipt with error performative.""" - - error_message = LedgerApiMessage(LedgerApiMessage.Performative.ERROR, code=0) # type: ignore - side_effect = mock_yield_and_return(error_message) - with as_context( - mock.patch.object(self.behaviour, "_send_transaction_receipt_request"), - mock.patch.object( - self.behaviour, "wait_for_message", side_effect=side_effect - ), - ): - gen = self.behaviour.get_transaction_receipt("tx_digest") - try_send(gen) - try_send(gen) - assert "Error when requesting transaction receipt" in caplog.text - - @pytest.mark.parametrize("contract_address", [None, "contract_address"]) - def test_get_contract_api_response(self, contract_address: Optional[str]) -> None: - """Test 'get_contract_api_response'.""" - with mock.patch.object( - self.behaviour.context.contract_api_dialogues, - "create", - return_value=(MagicMock(), MagicMock()), - ), mock.patch.object(behaviour_utils, "Terms"), mock.patch.object( - BaseBehaviour, "_send_transaction_signing_request" - ), mock.patch.object( - BaseBehaviour, "_send_transaction_request" - ): - gen = self.behaviour.get_contract_api_response( - MagicMock(), contract_address, "contract_id", "contract_callable" - ) - # first trigger - try_send(gen, obj=None) - # wait for message - try_send(gen, obj=MagicMock()) - - @mock.patch.object( - BaseBehaviour, "_build_http_request_message", return_value=(None, None) - ) - def test_get_status(self, _: mock.Mock) -> None: - """Test '_get_status'.""" - expected_result = json.dumps("Test result.").encode() - - def dummy_do_request(*_: Any) -> Generator[None, None, MagicMock]: - """Dummy `_do_request` method.""" - yield - return mock.MagicMock(body=expected_result) - - with mock.patch.object( - BaseBehaviour, "_do_request", side_effect=dummy_do_request - ): - get_status_generator = self.behaviour._get_status() - next(get_status_generator) - with pytest.raises(StopIteration) as e: - next(get_status_generator) - res = e.value.args[0] - assert isinstance(res, MagicMock) - assert res.body == expected_result - - def test_get_netinfo(self) -> None: - """Test _get_netinfo method""" - dummy_res = { - "result": { - "n_peers": "1", - } - } - expected_result = json.dumps(dummy_res).encode() - - def dummy_do_request(*_: Any) -> Generator[None, None, MagicMock]: - """Dummy `_do_request` method.""" - yield - return mock.MagicMock(body=expected_result) - - with mock.patch.object( - BaseBehaviour, "_do_request", side_effect=dummy_do_request - ): - get_netinfo_generator = self.behaviour._get_netinfo() - next(get_netinfo_generator) - with pytest.raises(StopIteration) as e: - next(get_netinfo_generator) - res = e.value.args[0] - assert isinstance(res, MagicMock) - assert res.body == expected_result - - @pytest.mark.parametrize( - ("num_peers", "expected_num_peers", "netinfo_status_code"), - [ - ("0", 1, 200), - ("0", None, 500), - ("0", None, None), - (None, None, 200), - ], - ) - def test_num_active_peers( - self, - num_peers: Optional[str], - expected_num_peers: Optional[int], - netinfo_status_code: Optional[int], - ) -> None: - """Test num_active_peers.""" - dummy_res = { - "result": { - "n_peers": num_peers, - } - } - - def dummy_get_netinfo(*_: Any) -> Generator[None, None, MagicMock]: - """Dummy `_get_netinfo` method.""" - yield - - if netinfo_status_code is None: - raise TimeoutException() - - return mock.MagicMock( - status_code=netinfo_status_code, - body=json.dumps(dummy_res).encode(), - ) - - with mock.patch.object( - BaseBehaviour, - "_get_netinfo", - side_effect=dummy_get_netinfo, - ): - num_active_peers_generator = self.behaviour.num_active_peers() - next(num_active_peers_generator) - with pytest.raises(StopIteration) as e: - next(num_active_peers_generator) - actual_num_peers = e.value.value - assert actual_num_peers == expected_num_peers - - def test_default_callback_request_stopped(self) -> None: - """Test 'default_callback_request' when stopped.""" - message = MagicMock() - current_behaviour = self.behaviour - with mock.patch.object(self.behaviour.context.logger, "debug") as debug_mock: - self.behaviour.get_callback_request()(message, current_behaviour) - debug_mock.assert_called_with( - "Dropping message as behaviour has stopped: %s", message - ) - - def test_default_callback_late_arriving_message(self, *_: Any) -> None: - """Test 'default_callback_request' when a message arrives late.""" - self.behaviour._AsyncBehaviour__stopped = False - message = MagicMock() - current_behaviour = MagicMock() - with mock.patch.object(self.behaviour.context.logger, "warning") as info_mock: - self.behaviour.get_callback_request()(message, current_behaviour) - info_mock.assert_called_with( - "No callback defined for request with nonce: " - f"{message.dialogue_reference.__getitem__()}, " - f"arriving for behaviour: {self.behaviour.behaviour_id}" - ) - - def test_default_callback_request_waiting_message(self, *_: Any) -> None: - """Test 'default_callback_request' when waiting message.""" - self.behaviour._AsyncBehaviour__stopped = False # type: ignore - self.behaviour._AsyncBehaviour__state = ( # type: ignore - AsyncBehaviour.AsyncState.WAITING_MESSAGE - ) - message = MagicMock() - current_behaviour = self.behaviour - self.behaviour.get_callback_request()(message, current_behaviour) - - def test_default_callback_request_else(self, *_: Any) -> None: - """Test 'default_callback_request' else branch.""" - self.behaviour._AsyncBehaviour__stopped = False # type: ignore - message = MagicMock() - current_behaviour = self.behaviour - with mock.patch.object( - self.behaviour.context.logger, "warning" - ) as warning_mock: - self.behaviour.get_callback_request()(message, current_behaviour) - warning_mock.assert_called_with( - "Could not send message to FSMBehaviour: %s", message - ) - - def test_stop(self) -> None: - """Test the stop method.""" - self.behaviour.stop() - - @pytest.mark.parametrize( - "performative", - ( - TendermintMessage.Performative.GET_GENESIS_INFO, - TendermintMessage.Performative.GET_RECOVERY_PARAMS, - ), - ) - @pytest.mark.parametrize( - "address_to_acn_deliverable, n_pending", - ( - ({}, 0), - ({i: None for i in range(3)}, 3), - ({0: "test", 1: None, 2: None}, 2), - ({i: "test" for i in range(3)}, 0), - ), - ) - def test_acn_request_from_pending( - self, - performative: TendermintMessage.Performative, - address_to_acn_deliverable: Dict[str, Any], - n_pending: int, - ) -> None: - """Test the `_acn_request_from_pending` method.""" - self.behaviour.context.state.address_to_acn_deliverable = ( - address_to_acn_deliverable - ) - gen = self.behaviour._acn_request_from_pending(performative) - - if n_pending == 0: - with pytest.raises(StopIteration): - next(gen) - return - - with mock.patch.object( - self.behaviour.context.tendermint_dialogues, - "create", - return_value=(MagicMock(), MagicMock()), - ) as dialogues_mock: - dialogues_mock.assert_not_called() - self.behaviour.context.outbox.put_message = MagicMock() - self.behaviour.context.outbox.put_message.assert_not_called() - - next(gen) - - dialogues_expected_calls = tuple( - mock.call(counterparty=address, performative=performative) - for address, deliverable in address_to_acn_deliverable.items() - if deliverable is None - ) - dialogues_mock.assert_has_calls(dialogues_expected_calls) - assert self.behaviour.context.outbox.put_message.call_count == len( - dialogues_expected_calls - ) - - time.sleep(self.behaviour.params.sleep_time) - with pytest.raises(StopIteration): - next(gen) - - @pytest.mark.parametrize( - "performative", - ( - TendermintMessage.Performative.GET_GENESIS_INFO, - TendermintMessage.Performative.GET_RECOVERY_PARAMS, - ), - ) - @pytest.mark.parametrize( - "address_to_acn_deliverable_per_attempt, expected_result", - ( - ( - tuple({"address": None} for _ in range(10)), - None, - ), # an example in which no agent responds - ( - ( - {f"address{i}": None for i in range(3)}, - {"address1": None, "address2": "test", "address3": None}, - ) - + tuple( - {"address1": None, "address2": "test", "address3": "malicious"} - for _ in range(8) - ), - None, - ), # an example in which no majority is reached - ( - tuple({f"address{i}": None for i in range(3)} for _ in range(3)) - + ({"address1": "test", "address2": "test", "address3": None},), - "test", - ), # an example in which majority is reached during the 4th ACN attempt - ), - ) - def test_perform_acn_request( - self, - performative: TendermintMessage.Performative, - address_to_acn_deliverable_per_attempt: Tuple[Dict[str, Any], ...], - expected_result: Any, - ) -> None: - """Test the `_perform_acn_request` method.""" - final_attempt_idx = len(address_to_acn_deliverable_per_attempt) - 1 - gen = self.behaviour._perform_acn_request(performative) - - with mock.patch.object( - self.behaviour, - "_acn_request_from_pending", - side_effect=dummy_generator_wrapper(), - ) as _acn_request_from_pending_mock: - for i in range(self.behaviour.params.max_attempts): - acn_result = expected_result if i == final_attempt_idx + 1 else None - with mock.patch.object( - self.behaviour.context.state, - "get_acn_result", - return_value=acn_result, - ): - if i != final_attempt_idx + 1: - self.behaviour.context.state.address_to_acn_deliverable = ( - address_to_acn_deliverable_per_attempt[i] - ) - next(gen) - continue - - try: - next(gen) - except StopIteration as exc: - assert exc.value == expected_result - else: - raise AssertionError( - "The `_perform_acn_request` was expected to yield for the last time." - ) - - break - - n_expected_calls = final_attempt_idx + 1 - expected_calls = tuple( - mock.call(performative) for _ in range(n_expected_calls) - ) - assert _acn_request_from_pending_mock.call_count == n_expected_calls - _acn_request_from_pending_mock.assert_has_calls(expected_calls) - - @pytest.mark.parametrize("expected_result", (True, False)) - def test_request_recovery_params(self, expected_result: bool) -> None: - """Test `request_recovery_params`.""" - acn_result = "not None ACN result" if expected_result else None - request_recovery_params = self.behaviour.request_recovery_params(False) - - with mock.patch.object( - self.behaviour, - "_perform_acn_request", - side_effect=dummy_generator_wrapper(acn_result), - ) as perform_acn_request_mock: - next(request_recovery_params) - - try: - next(request_recovery_params) - except StopIteration as exc: - assert exc.value is expected_result - else: - raise AssertionError( - "The `request_recovery_params` was expected to yield for the last time." - ) - - perform_acn_request_mock.assert_called_once_with( - TendermintMessage.Performative.GET_RECOVERY_PARAMS - ) - - def test_start_reset(self) -> None: - """Test the `_start_reset` method.""" - with mock.patch.object( - BaseBehaviour, - "wait_from_last_timestamp", - new_callable=lambda *_: dummy_generator_wrapper(), - ): - res = self.behaviour._start_reset() - for _ in range(2): - next(res) - assert self.behaviour._check_started is not None - assert self.behaviour._check_started <= datetime.now() - assert self.behaviour._timeout == self.behaviour.params.max_healthcheck - assert not self.behaviour._is_healthy - - def test_end_reset(self) -> None: - """Test the `_end_reset` method.""" - self.behaviour._end_reset() - assert self.behaviour._check_started is None - assert self.behaviour._timeout == -1.0 - assert self.behaviour._is_healthy - - @pytest.mark.parametrize( - "check_started, is_healthy, timeout, expiration_expected", - ( - (None, True, 0, False), - (None, False, 0, False), - (datetime(1, 1, 1), True, 0, False), - (datetime.now(), False, 3000, False), - (datetime(1, 1, 1), False, 0, True), - ), - ) - def test_is_timeout_expired( - self, - check_started: Optional[datetime], - is_healthy: bool, - timeout: float, - expiration_expected: bool, - ) -> None: - """Test the `_is_timeout_expired` method.""" - self.behaviour._check_started = check_started - self.behaviour._is_healthy = is_healthy - self.behaviour._timeout = timeout - assert self.behaviour._is_timeout_expired() == expiration_expected - - @pytest.mark.parametrize("default", (True, False)) - @given( - st.datetimes( - min_value=MIN_DATETIME_WINDOWS, - max_value=MAX_DATETIME_WINDOWS, - ), - st.integers(), - st.integers(), - st.integers(), - ) - def test_get_reset_params( - self, - default: bool, - timestamp: datetime, - height: int, - interval: int, - period: int, - ) -> None: - """Test `_get_reset_params` method.""" - self.context_mock.state.round_sequence.last_round_transition_timestamp = ( - timestamp - ) - self.context_mock.state.round_sequence.last_round_transition_tm_height = height - self.behaviour.params.reset_pause_duration = interval - self.context_state_synchronized_data_mock.period_count = period - - actual = self.behaviour._get_reset_params(default) - - if default: - assert actual is None - - else: - initial_height = INITIAL_HEIGHT - genesis_time = timestamp.astimezone(pytz.UTC).strftime(GENESIS_TIME_FMT) - period_count = str(period) - - expected = { - "genesis_time": genesis_time, - "initial_height": initial_height, - "period_count": period_count, - } - - assert actual == expected - - @mock.patch.object(BaseBehaviour, "_start_reset") - @mock.patch.object(BaseBehaviour, "_is_timeout_expired") - def test_reset_tendermint_with_wait_timeout_expired(self, *_: mock.Mock) -> None: - """Test tendermint reset.""" - with pytest.raises(RuntimeError, match="Error resetting tendermint node."): - next(self.behaviour.reset_tendermint_with_wait()) - - @mock.patch.object(BaseBehaviour, "_start_reset") - @mock.patch.object( - BaseBehaviour, "_build_http_request_message", return_value=(None, None) - ) - @pytest.mark.parametrize( - "reset_response, status_response, local_height, on_startup, n_iter, expecting_success", - ( - ( - {"message": "Tendermint reset was successful.", "status": True}, - {"result": {"sync_info": {"latest_block_height": 1}}}, - 1, - False, - 3, - True, - ), - ( - {"message": "Tendermint reset was successful.", "status": True}, - {"result": {"sync_info": {"latest_block_height": 1}}}, - 1, - True, - 2, - True, - ), - ( - { - "message": "Tendermint reset was successful.", - "status": True, - "is_replay": True, - }, - {"result": {"sync_info": {"latest_block_height": 1}}}, - 1, - False, - 3, - True, - ), - ( - {"message": "Tendermint reset was successful.", "status": True}, - {"result": {"sync_info": {"latest_block_height": 1}}}, - 3, - False, - 3, - False, - ), - ( - {"message": "Error resetting tendermint.", "status": False}, - {}, - 0, - False, - 2, - False, - ), - ("wrong_response", {}, 0, False, 2, False), - ( - {"message": "Reset Successful.", "status": True}, - "not_accepting_txs_yet", - 0, - False, - 3, - False, - ), - ), - ) - def test_reset_tendermint_with_wait( - self, - build_http_request_message_mock: mock.Mock, - _start_reset: mock.Mock, - reset_response: Union[Dict[str, Union[bool, str]], str], - status_response: Union[Dict[str, Union[int, str]], str], - local_height: int, - on_startup: bool, - n_iter: int, - expecting_success: bool, - ) -> None: - """Test tendermint reset.""" - - def dummy_do_request(*_: Any) -> Generator[None, None, MagicMock]: - """Dummy `_do_request` method.""" - yield - if reset_response == "wrong_response": - return mock.MagicMock(body=b"") - return mock.MagicMock(body=json.dumps(reset_response).encode()) - - def dummy_get_status(*_: Any) -> Generator[None, None, MagicMock]: - """Dummy `_get_status` method.""" - yield - if status_response == "not_accepting_txs_yet": - return mock.MagicMock(body=b"") - return mock.MagicMock(body=json.dumps(status_response).encode()) - - period_count_mock = MagicMock() - self.context_state_synchronized_data_mock.period_count = period_count_mock - self.behaviour.params.reset_pause_duration = 1 - with mock.patch.object( - BaseBehaviour, "_is_timeout_expired", return_value=False - ), mock.patch.object( - BaseBehaviour, - "wait_from_last_timestamp", - new_callable=lambda *_: dummy_generator_wrapper(), - ), mock.patch.object( - BaseBehaviour, "_do_request", new_callable=lambda *_: dummy_do_request - ), mock.patch.object( - BaseBehaviour, "_get_status", new_callable=lambda *_: dummy_get_status - ), mock.patch.object( - BaseBehaviour, "sleep", new_callable=lambda *_: dummy_generator_wrapper() - ): - self.behaviour.context.state.round_sequence.height = local_height - reset = self.behaviour.reset_tendermint_with_wait(on_startup=on_startup) - for _ in range(n_iter): - next(reset) - initial_height = INITIAL_HEIGHT - genesis_time = self.behaviour.context.state.round_sequence.last_round_transition_timestamp.astimezone( - pytz.UTC - ).strftime( - "%Y-%m-%dT%H:%M:%S.%fZ" - ) - - expected_parameters = ( - { - "genesis_time": genesis_time, - "initial_height": initial_height, - "period_count": str(period_count_mock), - } - if not on_startup - else None - ) - - build_http_request_message_mock.assert_called_with( - "GET", - self.behaviour.context.params.tendermint_com_url + "/hard_reset", - parameters=expected_parameters, - ) - - should_be_healthy = isinstance(reset_response, dict) and reset_response.get( - "status", False - ) - assert self.behaviour._is_healthy is should_be_healthy - - # perform the last iteration which also returns the result - try: - next(reset) - except StopIteration as e: - assert e.value == expecting_success - if expecting_success: - # upon having a successful reset we expect the reset params of that - # reset to be stored in the shared state, as they could be used - # later for performing hard reset in cases when the agent <-> tendermint - # communication is broken - shared_state = cast(SharedState, self.behaviour.context.state) - tm_recovery_params = shared_state.tm_recovery_params - assert tm_recovery_params.reset_params == expected_parameters - assert ( - tm_recovery_params.round_count - == shared_state.synchronized_data.db.round_count - 1 - ) - assert ( - tm_recovery_params.reset_from_round - == self.behaviour.matching_round.auto_round_id() - ) - assert not self.behaviour._is_healthy - else: - pytest.fail("`reset_tendermint_with_wait` did not finish!") - - @given(st.binary()) - def test_fuzz_submit_tx(self, input_bytes: bytes) -> None: - """Fuzz '_submit_tx'. - - Mock context manager decorators don't work here. - - :param input_bytes: fuzz input - """ - self.behaviour._submit_tx(input_bytes) - - -def test_degenerate_behaviour_async_act() -> None: - """Test DegenerateBehaviour.async_act.""" - - class ConcreteDegenerateBehaviour(DegenerateBehaviour): - """Concrete DegenerateBehaviour class.""" - - behaviour_id = "concrete_degenerate_behaviour" - matching_round = MagicMock() - sleep_time_before_exit = 0.01 - - context = MagicMock() - # this is needed to trigger execution of async_act - context.state.round_sequence.syncing_up = False - context.state.round_sequence.block_stall_deadline_expired = False - behaviour = ConcreteDegenerateBehaviour( - name=ConcreteDegenerateBehaviour.auto_behaviour_id(), skill_context=context - ) - with pytest.raises( - SystemExit, - ): - behaviour.act() - time.sleep(0.02) - behaviour.act() - - -def test_make_degenerate_behaviour() -> None: - """Test 'make_degenerate_behaviour'.""" - - class FinalRound(DegenerateRound, ABC): - """A final round for testing.""" - - new_cls = make_degenerate_behaviour(FinalRound) - - assert isinstance(new_cls, type) - assert issubclass(new_cls, DegenerateBehaviour) - assert new_cls.matching_round == FinalRound - - assert ( - new_cls.auto_behaviour_id() - == f"degenerate_behaviour_{FinalRound.auto_round_id()}" - ) - - -class TestTmManager: - """Class to test the TmManager behaviour.""" - - _DUMMY_CONSENSUS_THRESHOLD = 3 - - def setup(self) -> None: - """Set up the tests.""" - self.context_mock = MagicMock() - self.context_params_mock = MagicMock( - request_timeout=_DEFAULT_REQUEST_TIMEOUT, - request_retry_delay=_DEFAULT_REQUEST_RETRY_DELAY, - tx_timeout=_DEFAULT_TX_TIMEOUT, - max_attempts=_DEFAULT_TX_MAX_ATTEMPTS, - consensus_params=MagicMock( - consensus_threshold=self._DUMMY_CONSENSUS_THRESHOLD - ), - ) - self.context_state_synchronized_data_mock = MagicMock( - consensus_threshold=self._DUMMY_CONSENSUS_THRESHOLD - ) - self.context_mock.params = self.context_params_mock - self.context_mock.state.synchronized_data = ( - self.context_state_synchronized_data_mock - ) - self.recovery_params = TendermintRecoveryParams(MagicMock()) - self.context_mock.state.tm_recovery_params = self.recovery_params - self.context_mock.state.round_sequence.current_round_id = "round_a" - self.context_mock.state.round_sequence.syncing_up = False - self.context_mock.state.round_sequence.block_stall_deadline_expired = False - self.context_mock.http_dialogues = HttpDialogues() - self.context_mock.handlers.__dict__ = {"http": MagicMock()} - self.tm_manager = TmManager(name="", skill_context=self.context_mock) - self.tm_manager._max_reset_retry = 1 - self.tm_manager.synchronized_data.max_participants = 3 # type: ignore - - def test_async_act(self) -> None: - """Test the async_act method of the TmManager.""" - self.tm_manager.act_wrapper() - with pytest.raises( - SystemExit, - ): - self.tm_manager.act_wrapper() - - @given(latest_block_height=st.integers(min_value=0)) - @pytest.mark.parametrize( - "acn_communication_success", - ( - True, - False, - ), - ) - @pytest.mark.parametrize( - "gentle_reset_attempted", - ( - True, - False, - ), - ) - @pytest.mark.parametrize( - ("tm_reset_success", "num_active_peers"), - [ - (True, 4), - (False, 4), - (True, 2), - (False, None), - ], - ) - def test_handle_unhealthy_tm( - self, - latest_block_height: int, - acn_communication_success: bool, - gentle_reset_attempted: bool, - tm_reset_success: bool, - num_active_peers: Optional[int], - ) -> None: - """Test _handle_unhealthy_tm.""" - - self.tm_manager.gentle_reset_attempted = gentle_reset_attempted - self.tm_manager.context.state.round_sequence.height = latest_block_height - - def mock_sleep(_seconds: int) -> Generator: - """A method that mocks sleep.""" - return - yield - - def dummy_do_request(*_: Any) -> Generator[None, None, MagicMock]: - """Dummy `_do_request` method.""" - yield - return mock.MagicMock() - - def dummy_get_status(*_: Any) -> Generator[None, None, MagicMock]: - """Dummy `_get_status` method.""" - yield - return mock.MagicMock( - body=json.dumps( - { - "result": { - "sync_info": {"latest_block_height": latest_block_height} - } - } - ).encode() - ) - - gen = self.tm_manager._handle_unhealthy_tm() - with mock.patch.object( - self.tm_manager, - "reset_tendermint_with_wait", - side_effect=yield_and_return_bool_wrapper(tm_reset_success), - ), mock.patch.object( - self.tm_manager, - "num_active_peers", - side_effect=yield_and_return_int_wrapper(num_active_peers), - ), mock.patch.object( - self.tm_manager, "sleep", side_effect=mock_sleep - ), mock.patch.object( - BaseBehaviour, - "request_recovery_params", - side_effect=dummy_generator_wrapper(acn_communication_success), - ), mock.patch.object( - BaseBehaviour, "_do_request", new_callable=lambda *_: dummy_do_request - ), mock.patch.object( - BaseBehaviour, "_get_status", new_callable=lambda *_: dummy_get_status - ), mock.patch.object( - self.tm_manager.round_sequence, "set_block_stall_deadline" - ) as set_block_stall_deadline_mock: - next(gen) - - if not gentle_reset_attempted: - next(gen) - assert self.tm_manager.gentle_reset_attempted - with pytest.raises(StopIteration): - next(gen) - set_block_stall_deadline_mock.assert_called_once() - assert self.tm_manager.gentle_reset_attempted - return - - if not acn_communication_success: - with pytest.raises(StopIteration): - next(gen) - set_block_stall_deadline_mock.assert_not_called() - return - - next(gen) - with pytest.raises(StopIteration): - next(gen) - - set_block_stall_deadline_mock.assert_not_called() - assert self.tm_manager.informed is True - - @pytest.mark.parametrize( - "n_repetitions", - ( - 1, - 2, - 1000, - ), - ) - def test_handle_unhealthy_tm_logging(self, n_repetitions: int) -> None: - """Verify if unintended logging repetition occurs during the execution of `_handle_unhealthy_tm`.""" - - self.tm_manager.gentle_reset_attempted = False - self.tm_manager.context.state.round_sequence.height = 10 - - def mock_sleep(_seconds: int) -> Generator: - """A method that mocks sleep.""" - return - yield - - def dummy_do_request(*_: Any) -> Generator[None, None, MagicMock]: - """Dummy `_do_request` method.""" - yield - return mock.MagicMock() - - def dummy_get_status(*_: Any) -> Generator[None, None, MagicMock]: - """Dummy `_get_status` method.""" - yield - return mock.MagicMock( - body=json.dumps( - {"result": {"sync_info": {"latest_block_height": 0}}} - ).encode() - ) - - with mock.patch.object( - self.tm_manager, - "reset_tendermint_with_wait", - side_effect=yield_and_return_bool_wrapper(True), - ), mock.patch.object( - self.tm_manager, - "num_active_peers", - side_effect=yield_and_return_int_wrapper(4), - ), mock.patch.object( - self.tm_manager, "sleep", side_effect=mock_sleep - ), mock.patch.object( - BaseBehaviour, "_do_request", new_callable=lambda *_: dummy_do_request - ), mock.patch.object( - BaseBehaviour, "_get_status", new_callable=lambda *_: dummy_get_status - ): - assert self.tm_manager.informed is False - for _ in range(n_repetitions): - gen = self.tm_manager._handle_unhealthy_tm() - next(gen) - assert self.tm_manager.informed is True - - @pytest.mark.parametrize( - "expected_reset_params", - ( - {"genesis_time": "genesis-time", "initial_height": "1"}, - None, - ), - ) - def test_get_reset_params( - self, expected_reset_params: Optional[Dict[str, str]] - ) -> None: - """Test that reset params returns the correct params.""" - self.context_mock.state.tm_recovery_params = TendermintRecoveryParams( - reset_from_round="does not matter", reset_params=expected_reset_params - ) - actual_reset_params = self.tm_manager._get_reset_params(False) - assert expected_reset_params == actual_reset_params - - # setting the "default" arg to true should have no effect - actual_reset_params = self.tm_manager._get_reset_params(True) - assert expected_reset_params == actual_reset_params - - def test_sleep_after_hard_reset(self) -> None: - """Check that hard_reset_sleep returns the expected amount of time.""" - expected = self.tm_manager._hard_reset_sleep - actual = self.tm_manager.hard_reset_sleep - assert actual == expected - - @pytest.mark.parametrize( - ("state", "notified", "message", "num_iter"), - [ - (AsyncBehaviour.AsyncState.READY, False, None, 1), - (AsyncBehaviour.AsyncState.WAITING_MESSAGE, True, Message(), 2), - (AsyncBehaviour.AsyncState.WAITING_MESSAGE, True, Message(), 1), - ], - ) - def test_try_fix( - self, - state: AsyncBehaviour.AsyncState, - notified: bool, - message: Optional[Message], - num_iter: int, - ) -> None: - """Tests try_fix.""" - - def mock_handle_unhealthy_tm() -> Generator: - """A mock implementation of _handle_unhealthy_tm.""" - for _ in range(num_iter): - msg = yield - if msg is not None: - # if a message is recieved, the state of the behviour should be "RUNNING" - self.tm_manager._AsyncBehaviour__state = ( - AsyncBehaviour.AsyncState.RUNNING - ) - return - - with mock.patch.object( - self.tm_manager, - "_handle_unhealthy_tm", - side_effect=mock_handle_unhealthy_tm, - ): - # there is no active generator in the beginning - assert not self.tm_manager.is_acting - - # a generator should be created, and be active - self.tm_manager.try_fix() - assert self.tm_manager.is_acting - - # a message may (or may not) arrive - self.tm_manager._AsyncBehaviour__notified = notified - self.tm_manager._AsyncBehaviour__state = state - self.tm_manager._AsyncBehaviour__message = message - - # the generator has a single yield statement, - # a second try_fix() call should finish it - for _ in range(num_iter): - self.tm_manager.try_fix() - assert not self.tm_manager.is_acting, num_iter - - @pytest.mark.parametrize( - "state", - [ - AsyncBehaviour.AsyncState.WAITING_MESSAGE, - AsyncBehaviour.AsyncState.READY, - ], - ) - def test_get_callback_request(self, state: AsyncBehaviour.AsyncState) -> None: - """Tests get_callback_request.""" - self.tm_manager._AsyncBehaviour__state = state - dummy_msg, dummy_behaviour = MagicMock(), MagicMock() - callback_req = self.tm_manager.get_callback_request() - with mock.patch.object(self.tm_manager, "try_send"): - callback_req(dummy_msg, dummy_behaviour) - - def test_is_acting(self) -> None: - """Test is_acting.""" - self.tm_manager._active_generator = MagicMock() - assert self.tm_manager.is_acting - - self.tm_manager._active_generator = None - assert not self.tm_manager.is_acting - - -def test_meta_base_behaviour_when_instance_not_subclass_of_base_behaviour() -> None: - """Test instantiation of meta class when instance not a subclass of BaseBehaviour.""" - - class MyBaseBehaviour(metaclass=_MetaBaseBehaviour): - pass - - -def test_base_behaviour_instantiation_without_attributes_raises_error() -> None: - """Test that definition of concrete subclass of BaseBehaviour without attributes raises error.""" - with pytest.raises(BaseBehaviourInternalError): - - class MyBaseBehaviour(BaseBehaviour): - pass - - -class TestIPFSBehaviour: - """Test IPFSBehaviour tests.""" - - def setup(self) -> None: - """Sets up the tests.""" - self.context_mock = MagicMock() - self.context_mock.ipfs_dialogues = IpfsDialogues( - connection_id=str(IPFS_CONNECTION_ID) - ) - self.behaviour = BehaviourATest(name="", skill_context=self.context_mock) - - def test_build_ipfs_message(self) -> None: - """Tests _build_ipfs_message.""" - res = self.behaviour._build_ipfs_message(IpfsMessage.Performative.GET_FILES) # type: ignore - assert res is not None - - def test_build_ipfs_store_file_req(self) -> None: - """Tests _build_ipfs_store_file_req.""" - with mock.patch.object( - IPFSInteract, "store", return_value=MagicMock() - ) as mock_store: - res = self.behaviour._build_ipfs_store_file_req("dummy_filename", {}) - mock_store.assert_called() - assert res is not None - - def test_build_ipfs_get_file_req(self) -> None: - """Tests _build_ipfs_get_file_req.""" - res = self.behaviour._build_ipfs_get_file_req("dummy_ipfs_hash") - assert res is not None - - def test_deserialize_ipfs_objects(self) -> None: - """Tests _deserialize_ipfs_objects""" - with mock.patch.object( - IPFSInteract, "load", return_value=MagicMock() - ) as mock_load: - res = self.behaviour._deserialize_ipfs_objects({}) - mock_load.assert_called() - assert res is not None diff --git a/packages/valory/skills/abstract_round_abci/tests/test_common.py b/packages/valory/skills/abstract_round_abci/tests/test_common.py deleted file mode 100644 index 427aa81..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/test_common.py +++ /dev/null @@ -1,407 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test the common.py module of the skill.""" - -import random -import re -from typing import ( - Any, - Callable, - Dict, - FrozenSet, - Generator, - Optional, - Set, - Type, - TypeVar, - Union, -) -from unittest import mock -from unittest.mock import MagicMock - -import pytest - -from packages.valory.protocols.ledger_api.message import LedgerApiMessage -from packages.valory.skills.abstract_round_abci.common import ( - RandomnessBehaviour, - SelectKeeperBehaviour, - random_selection, -) -from packages.valory.skills.abstract_round_abci.models import BaseParams -from packages.valory.skills.abstract_round_abci.tests.conftest import irrelevant_config -from packages.valory.skills.abstract_round_abci.utils import VerifyDrand - - -ReturnValueType = TypeVar("ReturnValueType") - - -def test_random_selection() -> None: - """Test 'random_selection'""" - assert random_selection(elements=[0, 1, 2], randomness=0.25) == 0 - assert random_selection(elements=[0, 1, 2], randomness=0.5) == 1 - assert random_selection(elements=[0, 1, 2], randomness=0.75) == 2 - - with pytest.raises( - ValueError, match=re.escape("Randomness should lie in the [0,1) interval") - ): - random_selection(elements=[0, 1], randomness=-1) - - with pytest.raises( - ValueError, match=re.escape("Randomness should lie in the [0,1) interval") - ): - random_selection(elements=[0, 1], randomness=1) - - with pytest.raises( - ValueError, match=re.escape("Randomness should lie in the [0,1) interval") - ): - random_selection(elements=[0, 1], randomness=2) - - with pytest.raises(ValueError, match="No elements to randomly select among"): - random_selection(elements=[], randomness=0.5) - - -class DummyRandomnessBehaviour(RandomnessBehaviour): - """Dummy randomness behaviour.""" - - behaviour_id = "dummy_randomness" - payload_class = MagicMock() - matching_round = MagicMock() - - -class DummySelectKeeperBehaviour(SelectKeeperBehaviour): - """Dummy select keeper behaviour.""" - - behaviour_id = "dummy_select_keeper" - payload_class = MagicMock() - matching_round = MagicMock() - - -DummyBehaviourType = Union[DummyRandomnessBehaviour, DummySelectKeeperBehaviour] - - -class BaseDummyBehaviour: # pylint: disable=too-few-public-methods - """A Base dummy behaviour class.""" - - behaviour: DummyBehaviourType - dummy_behaviour_cls: Type[DummyBehaviourType] - - @classmethod - def setup_class(cls) -> None: - """Setup the test class.""" - cls.behaviour = cls.dummy_behaviour_cls( - name="test", - skill_context=MagicMock( - params=BaseParams( - name="test", - skill_context=MagicMock(), - service_id="test_id", - consensus=dict(max_participants=1), - **irrelevant_config, - ), - ), - ) - - -def dummy_generator( - return_value: ReturnValueType, -) -> Callable[[Any, Any], Generator[None, None, ReturnValueType]]: - """A method that returns a dummy generator which yields nothing once and then returns the given `return_value`.""" - - def dummy_generator_wrapped( - *_: Any, **__: Any - ) -> Generator[None, None, ReturnValueType]: - """A wrapped method which yields nothing once and then returns the given `return_value`.""" - yield - return return_value - - return dummy_generator_wrapped - - -def last_iteration(gen: Generator) -> None: - """Perform a generator iteration and ensure that it is the last one.""" - with pytest.raises(StopIteration): - next(gen) - - -class TestRandomnessBehaviour(BaseDummyBehaviour): - """Test `RandomnessBehaviour`.""" - - @classmethod - def setup_class(cls) -> None: - """Setup the test class.""" - cls.dummy_behaviour_cls = DummyRandomnessBehaviour - super().setup_class() - - @pytest.mark.parametrize( - "return_value, expected_hash", - ( - (MagicMock(performative=LedgerApiMessage.Performative.ERROR), None), - (MagicMock(state=MagicMock(body={"hash_key_is_not_in_body": ""})), None), - ( - MagicMock(state=MagicMock(body={"hash": "test_randomness"})), - { - "randomness": "d067b86fa5235e7e5225e8328e8faac5c279cbf57131d647e4da0a70df6d3d7b", - "round": 0, - }, - ), - ), - ) - def test_failsafe_randomness( - self, return_value: MagicMock, expected_hash: Optional[str] - ) -> None: - """Test `failsafe_randomness`.""" - gen = self.behaviour.failsafe_randomness() - - with mock.patch.object( - DummyRandomnessBehaviour, - "get_ledger_api_response", - dummy_generator(return_value), - ): - next(gen) - try: - next(gen) - except StopIteration as e: - assert e.value == expected_hash - else: - raise AssertionError( - "`get_ledger_api_response`'s generator should have been exhausted." - ) - - @pytest.mark.parametrize("randomness_response", ("test", None)) - @pytest.mark.parametrize("verified", (True, False)) - def test_get_randomness_from_api( - self, randomness_response: Optional[str], verified: bool - ) -> None: - """Test `get_randomness_from_api`.""" - # create a dummy `process_response` for `MagicMock`ed `randomness_api` - self.behaviour.context.randomness_api.process_response = ( - lambda res: res + "_processed" if res is not None else None - ) - gen = self.behaviour.get_randomness_from_api() - - with mock.patch.object( - DummyRandomnessBehaviour, - "get_http_response", - dummy_generator(randomness_response), - ), mock.patch.object( - VerifyDrand, - "verify", - return_value=(verified, "Error message."), - ): - next(gen) - try: - next(gen) - except StopIteration as e: - if randomness_response is None or not verified: - assert e.value is None - else: - assert e.value == randomness_response + "_processed" - else: - raise AssertionError( - "`get_randomness_from_api`'s generator should have been exhausted." - ) - - @pytest.mark.parametrize( - "retries_exceeded, failsafe_succeeds", - # (False, False) is not tested, because it does not make sense - ((True, False), (True, True), (False, True)), - ) - @pytest.mark.parametrize( - "observation", - ( - None, - {}, - { - "randomness": "d067b86fa5235e7e5225e8328e8faac5c279cbf57131d647e4da0a70df6d3d7b", - "round": 0, - }, - ), - ) - def test_async_act( - self, - retries_exceeded: bool, - failsafe_succeeds: bool, - observation: Optional[Dict[str, Union[str, int]]], - ) -> None: - """Test `async_act`.""" - # create a dummy `is_retries_exceeded` for `MagicMock`ed `randomness_api` - self.behaviour.context.randomness_api.is_retries_exceeded = ( - lambda: retries_exceeded - ) - gen = self.behaviour.async_act() - - with mock.patch.object( - self.behaviour, - "failsafe_randomness", - dummy_generator(observation), - ), mock.patch.object( - self.behaviour, - "get_randomness_from_api", - dummy_generator(observation), - ), mock.patch.object( - self.behaviour, - "send_a2a_transaction", - ) as send_a2a_transaction_mocked, mock.patch.object( - self.behaviour, - "wait_until_round_end", - ) as wait_until_round_end_mocked, mock.patch.object( - self.behaviour, - "set_done", - ) as set_done_mocked, mock.patch.object( - self.behaviour, - "sleep", - ) as sleep_mocked: - next(gen) - last_iteration(gen) - - if not failsafe_succeeds or failsafe_succeeds and observation is None: - return - - # here, the observation is retrieved from either `failsafe_randomness` or `get_randomness_from_api` - # depending on the test's parametrization - if not observation: - sleep_mocked.assert_called_once_with( - self.behaviour.context.randomness_api.retries_info.suggested_sleep_time - ) - self.behaviour.context.randomness_api.increment_retries.assert_called_once() - return - - send_a2a_transaction_mocked.assert_called_once() - wait_until_round_end_mocked.assert_called_once() - set_done_mocked.assert_called_once() - - def test_clean_up(self) -> None: - """Test `clean_up`.""" - self.behaviour.clean_up() - self.behaviour.context.randomness_api.reset_retries.assert_called_once() - - def teardown(self) -> None: - """Teardown run after each test method.""" - self.behaviour.context.randomness_api.increment_retries.reset_mock() - - -class TestSelectKeeperBehaviour(BaseDummyBehaviour): - """Tests for `SelectKeeperBehaviour`.""" - - @classmethod - def setup_class(cls) -> None: - """Setup the test class.""" - cls.dummy_behaviour_cls = DummySelectKeeperBehaviour - super().setup_class() - - @mock.patch.object(random, "shuffle", lambda do_not_shuffle: do_not_shuffle) - @pytest.mark.parametrize( - "participants, blacklisted_keepers, most_voted_keeper_address, expected_keeper", - ( - ( - frozenset((f"test_p{i}" for i in range(4))), - set(), - "test_p0", - "test_p1", - ), - ( - frozenset((f"test_p{i}" for i in range(4))), - set(), - "test_p1", - "test_p2", - ), - ( - frozenset((f"test_p{i}" for i in range(4))), - set(), - "test_p2", - "test_p3", - ), - ( - frozenset((f"test_p{i}" for i in range(4))), - set(), - "test_p3", - "test_p0", - ), - ( - frozenset((f"test_p{i}" for i in range(4))), - {f"test_p{i}" for i in range(1)}, - "test_p1", - "test_p2", - ), - ( - frozenset((f"test_p{i}" for i in range(4))), - {f"test_p{i}" for i in range(4)}, - "", - "", - ), - ), - ) - def test_select_keeper( - self, - participants: FrozenSet[str], - blacklisted_keepers: Set[str], - most_voted_keeper_address: str, # pylint: disable=unused-argument - expected_keeper: str, - ) -> None: - """Test `_select_keeper`.""" - for sync_data_name in ( - "participants", - "blacklisted_keepers", - "most_voted_keeper_address", - ): - setattr( - self.behaviour.context.state.synchronized_data, - sync_data_name, - locals()[sync_data_name], - ) - - select_keeper_method = ( - self.behaviour._select_keeper # pylint: disable=protected-access - ) - - if not participants - blacklisted_keepers: - with pytest.raises( - RuntimeError, - match="Cannot continue if all the keepers have been blacklisted!", - ): - select_keeper_method() - return - - with mock.patch("random.seed"): - actual_keeper = select_keeper_method() - assert actual_keeper == expected_keeper - - def test_async_act(self) -> None: - """Test `async_act`.""" - gen = self.behaviour.async_act() - - with mock.patch.object( - self.behaviour, - "_select_keeper", - return_value="test_keeper", - ), mock.patch.object( - self.behaviour, - "send_a2a_transaction", - ) as send_a2a_transaction_mocked, mock.patch.object( - self.behaviour, - "wait_until_round_end", - ) as wait_until_round_end_mocked, mock.patch.object( - self.behaviour, - "set_done", - ) as set_done_mocked: - last_iteration(gen) - send_a2a_transaction_mocked.assert_called_once() - wait_until_round_end_mocked.assert_called_once() - set_done_mocked.assert_called_once() diff --git a/packages/valory/skills/abstract_round_abci/tests/test_dialogues.py b/packages/valory/skills/abstract_round_abci/tests/test_dialogues.py deleted file mode 100644 index 12aeeaa..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/test_dialogues.py +++ /dev/null @@ -1,100 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test the dialogues.py module of the skill.""" - -# pylint: skip-file - -from enum import Enum -from typing import Type, cast -from unittest.mock import MagicMock - -import pytest -from aea.protocols.dialogue.base import Dialogues -from aea.skills.base import Model - -from packages.valory.connections.ipfs.connection import PUBLIC_ID as IPFS_CONNECTION_ID -from packages.valory.protocols.ipfs import IpfsMessage -from packages.valory.skills.abstract_round_abci.dialogues import ( - AbciDialogue, - AbciDialogues, - ContractApiDialogue, - ContractApiDialogues, - HttpDialogue, - HttpDialogues, - IpfsDialogues, - LedgerApiDialogue, - LedgerApiDialogues, - SigningDialogue, - SigningDialogues, - TendermintDialogue, - TendermintDialogues, -) - - -@pytest.mark.parametrize( - "dialogues_cls,expected_role_from_first_message", - [ - (AbciDialogues, AbciDialogue.Role.CLIENT), - (HttpDialogues, HttpDialogue.Role.CLIENT), - (SigningDialogues, SigningDialogue.Role.SKILL), - (LedgerApiDialogues, LedgerApiDialogue.Role.AGENT), - (ContractApiDialogues, ContractApiDialogue.Role.AGENT), - (TendermintDialogues, TendermintDialogue.Role.AGENT), - ], -) -def test_dialogues_creation( - dialogues_cls: Type[Model], expected_role_from_first_message: Enum -) -> None: - """Test XDialogues creations.""" - dialogues = cast(Dialogues, dialogues_cls(name="", skill_context=MagicMock())) - assert expected_role_from_first_message == dialogues._role_from_first_message( - MagicMock(), MagicMock() - ) - - -def test_ledger_api_dialogue() -> None: - """Test 'LedgerApiDialogue' creation.""" - dialogue = LedgerApiDialogue(MagicMock(), "", MagicMock()) - with pytest.raises(ValueError, match="Terms not set!"): - dialogue.terms - - expected_terms = MagicMock() - dialogue.terms = expected_terms - assert expected_terms == dialogue.terms - - -def test_contract_api_dialogue() -> None: - """Test 'ContractApiDialogue' creation.""" - dialogue = ContractApiDialogue(MagicMock(), "", MagicMock()) - with pytest.raises(ValueError, match="Terms not set!"): - dialogue.terms - - expected_terms = MagicMock() - dialogue.terms = expected_terms - assert expected_terms == dialogue.terms - - -def test_ipfs_dialogue() -> None: - """Test 'IpfsDialogues' creation.""" - dialogues = IpfsDialogues(name="", skill_context=MagicMock()) - dialogues.create( - counterparty=str(IPFS_CONNECTION_ID), - performative=IpfsMessage.Performative.GET_FILES, - ) diff --git a/packages/valory/skills/abstract_round_abci/tests/test_handlers.py b/packages/valory/skills/abstract_round_abci/tests/test_handlers.py deleted file mode 100644 index 415f11c..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/test_handlers.py +++ /dev/null @@ -1,596 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test the handlers.py module of the skill.""" - -# pylint: skip-file - -import json -import logging -from dataclasses import asdict -from datetime import datetime -from typing import Any, Dict, cast -from unittest import mock -from unittest.mock import MagicMock - -import pytest -from _pytest.logging import LogCaptureFixture -from aea.configurations.data_types import PublicId -from aea.protocols.base import Message - -from packages.valory.protocols.abci import AbciMessage -from packages.valory.protocols.abci.custom_types import ( - CheckTxType, - CheckTxTypeEnum, - ConsensusParams, - Evidences, - Header, - LastCommitInfo, - Timestamp, - ValidatorUpdates, -) -from packages.valory.protocols.http import HttpMessage -from packages.valory.protocols.tendermint import TendermintMessage -from packages.valory.skills.abstract_round_abci import handlers -from packages.valory.skills.abstract_round_abci.base import ( - ABCIAppInternalError, - AddBlockError, - ERROR_CODE, - OK_CODE, - SignatureNotValidError, - TransactionNotValidError, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - AbciDialogue, - AbciDialogues, - TendermintDialogue, - TendermintDialogues, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - ABCIRoundHandler, - AbstractResponseHandler, - TendermintHandler, - Transaction, - exception_to_info_msg, -) -from packages.valory.skills.abstract_round_abci.models import TendermintRecoveryParams -from packages.valory.skills.abstract_round_abci.test_tools.rounds import DummyRound - - -def test_exception_to_info_msg() -> None: - """Test 'exception_to_info_msg' helper function.""" - exception = Exception("exception message") - expected_string = f"{exception.__class__.__name__}: {str(exception)}" - actual_string = exception_to_info_msg(exception) - assert expected_string == actual_string - - -class TestABCIRoundHandler: - """Test 'ABCIRoundHandler'.""" - - def setup(self) -> None: - """Set up the tests.""" - self.context = MagicMock(skill_id=PublicId.from_str("dummy/skill:0.1.0")) - self.dialogues = AbciDialogues(name="", skill_context=self.context) - self.handler = ABCIRoundHandler(name="", skill_context=self.context) - self.context.state.round_sequence.height = 0 - self.context.state.round_sequence.root_hash = b"root_hash" - self.context.state.round_sequence.last_round_transition_timestamp = ( - datetime.now() - ) - - def test_info(self) -> None: - """Test the 'info' handler method.""" - message, dialogue = self.dialogues.create( - counterparty="", - performative=AbciMessage.Performative.REQUEST_INFO, - version="", - block_version=0, - p2p_version=0, - ) - response = self.handler.info( - cast(AbciMessage, message), cast(AbciDialogue, dialogue) - ) - assert response.performative == AbciMessage.Performative.RESPONSE_INFO - - @pytest.mark.parametrize("app_hash", (b"", b"test")) - def test_init_chain(self, app_hash: bytes) -> None: - """Test the 'init_chain' handler method.""" - time = Timestamp(0, 0) - consensus_params = ConsensusParams(*(mock.MagicMock() for _ in range(4))) - validators = ValidatorUpdates(mock.MagicMock()) - message, dialogue = self.dialogues.create( - counterparty="", - performative=AbciMessage.Performative.REQUEST_INIT_CHAIN, - time=time, - chain_id="test_chain_id", - consensus_params=consensus_params, - validators=validators, - app_state_bytes=b"", - initial_height=10, - ) - self.context.state.round_sequence.last_round_transition_root_hash = app_hash - response = self.handler.init_chain( - cast(AbciMessage, message), cast(AbciDialogue, dialogue) - ) - assert response.performative == AbciMessage.Performative.RESPONSE_INIT_CHAIN - assert response.validators == ValidatorUpdates([]) - assert response.app_hash == app_hash - - def test_begin_block(self) -> None: - """Test the 'begin_block' handler method.""" - header = Header(*(MagicMock() for _ in range(14))) - last_commit_info = LastCommitInfo(*(MagicMock() for _ in range(2))) - byzantine_validators = Evidences(MagicMock()) - message, dialogue = self.dialogues.create( - counterparty="", - performative=AbciMessage.Performative.REQUEST_BEGIN_BLOCK, - hash=b"", - header=header, - last_commit_info=last_commit_info, - byzantine_validators=byzantine_validators, - ) - response = self.handler.begin_block( - cast(AbciMessage, message), cast(AbciDialogue, dialogue) - ) - assert response.performative == AbciMessage.Performative.RESPONSE_BEGIN_BLOCK - - @mock.patch.object(handlers, "Transaction") - def test_check_tx(self, *_: Any) -> None: - """Test the 'check_tx' handler method.""" - message, dialogue = self.dialogues.create( - counterparty="", - performative=AbciMessage.Performative.REQUEST_CHECK_TX, - tx=b"", - type=CheckTxType(CheckTxTypeEnum.NEW), - ) - response = self.handler.check_tx( - cast(AbciMessage, message), cast(AbciDialogue, dialogue) - ) - assert response.performative == AbciMessage.Performative.RESPONSE_CHECK_TX - assert response.code == OK_CODE - - @mock.patch.object( - Transaction, - "decode", - side_effect=SignatureNotValidError, - ) - def test_check_tx_negative(self, *_: Any) -> None: - """Test the 'check_tx' handler method, negative case.""" - message, dialogue = self.dialogues.create( - counterparty="", - performative=AbciMessage.Performative.REQUEST_CHECK_TX, - tx=b"", - type=CheckTxType(CheckTxTypeEnum.NEW), - ) - response = self.handler.check_tx( - cast(AbciMessage, message), cast(AbciDialogue, dialogue) - ) - assert response.performative == AbciMessage.Performative.RESPONSE_CHECK_TX - assert response.code == ERROR_CODE - - @mock.patch.object(handlers, "Transaction") - def test_deliver_tx(self, *_: Any) -> None: - """Test the 'deliver_tx' handler method.""" - message, dialogue = self.dialogues.create( - counterparty="", - performative=AbciMessage.Performative.REQUEST_DELIVER_TX, - tx=b"", - ) - with mock.patch.object( - self.context.state.round_sequence, "add_pending_offence" - ) as mock_add_pending_offence: - response = self.handler.deliver_tx( - cast(AbciMessage, message), cast(AbciDialogue, dialogue) - ) - mock_add_pending_offence.assert_called_once() - - assert response.performative == AbciMessage.Performative.RESPONSE_DELIVER_TX - assert response.code == OK_CODE - - @mock.patch.object( - Transaction, - "decode", - side_effect=SignatureNotValidError, - ) - def test_deliver_tx_negative(self, *_: Any) -> None: - """Test the 'deliver_tx' handler method, negative case.""" - message, dialogue = self.dialogues.create( - counterparty="", - performative=AbciMessage.Performative.REQUEST_DELIVER_TX, - tx=b"", - ) - with mock.patch.object( - self.context.state.round_sequence, "add_pending_offence" - ) as mock_add_pending_offence: - response = self.handler.deliver_tx( - cast(AbciMessage, message), cast(AbciDialogue, dialogue) - ) - mock_add_pending_offence.assert_not_called() - - assert response.performative == AbciMessage.Performative.RESPONSE_DELIVER_TX - assert response.code == ERROR_CODE - - @mock.patch.object(handlers, "Transaction") - def test_deliver_bad_tx(self, *_: Any) -> None: - """Test the 'deliver_tx' handler method, when the transaction is not ok.""" - message, dialogue = self.dialogues.create( - counterparty="", - performative=AbciMessage.Performative.REQUEST_DELIVER_TX, - tx=b"", - ) - with mock.patch.object( - self.context.state.round_sequence, - "check_is_finished", - side_effect=TransactionNotValidError, - ), mock.patch.object( - self.context.state.round_sequence, "add_pending_offence" - ) as mock_add_pending_offence: - response = self.handler.deliver_tx( - cast(AbciMessage, message), cast(AbciDialogue, dialogue) - ) - mock_add_pending_offence.assert_called_once() - - assert response.performative == AbciMessage.Performative.RESPONSE_DELIVER_TX - assert response.code == ERROR_CODE - - @pytest.mark.parametrize("request_height", tuple(range(3))) - def test_end_block(self, request_height: int) -> None: - """Test the 'end_block' handler method.""" - message, dialogue = self.dialogues.create( - counterparty="", - performative=AbciMessage.Performative.REQUEST_END_BLOCK, - height=request_height, - ) - assert isinstance(message, AbciMessage) - assert isinstance(dialogue, AbciDialogue) - assert message.height == request_height - assert self.context.state.round_sequence.tm_height != request_height - response = self.handler.end_block(message, dialogue) - assert response.performative == AbciMessage.Performative.RESPONSE_END_BLOCK - assert self.context.state.round_sequence.tm_height == request_height - - def test_commit(self) -> None: - """Test the 'commit' handler method.""" - message, dialogue = self.dialogues.create( - counterparty="", - performative=AbciMessage.Performative.REQUEST_COMMIT, - ) - response = self.handler.commit( - cast(AbciMessage, message), cast(AbciDialogue, dialogue) - ) - assert response.performative == AbciMessage.Performative.RESPONSE_COMMIT - - def test_commit_negative(self) -> None: - """Test the 'commit' handler method, negative case.""" - self.context.state.round_sequence.commit.side_effect = AddBlockError() - message, dialogue = self.dialogues.create( - counterparty="", - performative=AbciMessage.Performative.REQUEST_COMMIT, - ) - with pytest.raises(AddBlockError): - self.handler.commit( - cast(AbciMessage, message), cast(AbciDialogue, dialogue) - ) - - -class ConcreteResponseHandler(AbstractResponseHandler): - """A concrete response handler for testing purposes.""" - - SUPPORTED_PROTOCOL = HttpMessage.protocol_id - allowed_response_performatives = frozenset({HttpMessage.Performative.RESPONSE}) - - -class TestAbstractResponseHandler: - """Test 'AbstractResponseHandler'.""" - - def setup(self) -> None: - """Set up the tests.""" - self.context = MagicMock() - self.handler = ConcreteResponseHandler(name="", skill_context=self.context) - - def test_handle(self) -> None: - """Test the 'handle' method.""" - callback = MagicMock() - request_reference = "reference" - self.context.requests.request_id_to_callback = {} - self.context.requests.request_id_to_callback[request_reference] = callback - with mock.patch.object( - self.handler, "_recover_protocol_dialogues" - ) as mock_dialogues_fn: - mock_dialogue = MagicMock() - mock_dialogue.dialogue_label.dialogue_reference = (request_reference, "") - mock_dialogues = MagicMock() - mock_dialogues.update = MagicMock(return_value=mock_dialogue) - mock_dialogues_fn.return_value = mock_dialogues - mock_message = MagicMock(performative=HttpMessage.Performative.RESPONSE) - self.handler.handle(mock_message) - callback.assert_called() - - @mock.patch.object( - AbstractResponseHandler, "_recover_protocol_dialogues", return_value=None - ) - def test_handle_negative_cannot_recover_dialogues(self, *_: Any) -> None: - """Test the 'handle' method, negative case (cannot recover dialogues).""" - self.handler.handle(MagicMock()) - - @mock.patch.object(AbstractResponseHandler, "_recover_protocol_dialogues") - def test_handle_negative_cannot_update_dialogues( - self, mock_dialogues_fn: Any - ) -> None: - """Test the 'handle' method, negative case (cannot update dialogues).""" - mock_dialogues = MagicMock(update=MagicMock(return_value=None)) - mock_dialogues_fn.return_value = mock_dialogues - self.handler.handle(MagicMock()) - - def test_handle_negative_performative_not_allowed(self) -> None: - """Test the 'handle' method, negative case (performative not allowed).""" - self.handler.handle(MagicMock()) - - def test_handle_negative_cannot_find_callback(self) -> None: - """Test the 'handle' method, negative case (cannot find callback).""" - self.context.requests.request_id_to_callback = {} - with pytest.raises( - ABCIAppInternalError, match="No callback defined for request with nonce: " - ): - self.handler.handle( - MagicMock(performative=HttpMessage.Performative.RESPONSE) - ) - - -class TestTendermintHandler: - """Test Tendermint Handler""" - - def setup(self) -> None: - """Set up the tests.""" - self.agent_name = "Alice" - self.context = MagicMock(skill_id=PublicId.from_str("dummy/skill:0.1.0")) - other_agents = ["Alice", "Bob", "Charlie"] - self.context.state = MagicMock(acn_container=lambda: other_agents) - self.handler = TendermintHandler(name="dummy", skill_context=self.context) - self.handler.context.logger = logging.getLogger() - self.dialogues = TendermintDialogues(name="dummy", skill_context=self.context) - - @property - def dummy_validator_config(self) -> Dict[str, Dict[str, str]]: - """Dummy validator config""" - return { - self.agent_name: { - "hostname": "localhost", - "address": "address", - "pub_key": "pub_key", - "peer_id": "peer_id", - } - } - - @staticmethod - def make_error_message() -> TendermintMessage: - """Make dummy error message""" - performative = TendermintMessage.Performative.ERROR - error_code = TendermintMessage.ErrorCode.INVALID_REQUEST - error_msg, error_data = "", {} # type: ignore - message = TendermintMessage( - performative, # type: ignore - error_code=error_code, - error_msg=error_msg, - error_data=error_data, - ) - message.sender = "Alice" - return message - - # pre-condition checks - def test_handle_unidentified_tendermint_dialogue( - self, caplog: LogCaptureFixture - ) -> None: - """Test unidentified tendermint dialogue""" - message = Message() - with mock.patch.object(self.handler.dialogues, "update", return_value=None): - self.handler.handle(message) - log_message = self.handler.LogMessages.unidentified_dialogue.value - assert log_message in caplog.text - - def test_handle_no_addresses_retrieved_yet(self, caplog: LogCaptureFixture) -> None: - """Test handle request no registered addresses""" - performative = TendermintMessage.Performative.GET_GENESIS_INFO - message = TendermintMessage(performative) # type: ignore - message.sender = "Alice" - self.handler.initial_tm_configs = {} - self.handler.handle(message) - log_message = self.handler.LogMessages.no_addresses_retrieved_yet.value - assert log_message in caplog.text - log_message = self.handler.LogMessages.sending_error_response.value - assert log_message in caplog.text - - def test_handle_not_in_registered_addresses( - self, caplog: LogCaptureFixture - ) -> None: - """Test handle response sender not in registered addresses""" - performative = TendermintMessage.Performative.GENESIS_INFO - message = TendermintMessage(performative, info="info") # type: ignore - message.sender = "NotAlice" - self.handler.handle(message) - log_message = self.handler.LogMessages.not_in_registered_addresses.value - assert log_message in caplog.text - - # request - def test_handle_get_genesis_info(self, caplog: LogCaptureFixture) -> None: - """Test handle request for genesis info""" - performative = TendermintMessage.Performative.GET_GENESIS_INFO - message = TendermintMessage(performative) # type: ignore - self.context.agent_address = message.sender = self.agent_name - self.handler.initial_tm_configs = self.dummy_validator_config - self.handler.handle(message) - log_message = self.handler.LogMessages.sending_request_response.value - assert log_message in caplog.text - - # response - def test_handle_response_invalid_addresses(self, caplog: LogCaptureFixture) -> None: - """Test handle response for genesis info with invalid address.""" - validator_config = self.dummy_validator_config - validator_config[self.agent_name]["hostname"] = "random" - performative = TendermintMessage.Performative.GENESIS_INFO - info = json.dumps(validator_config[self.agent_name]) - message = TendermintMessage(performative, info=info) # type: ignore - self.context.agent_address = message.sender = self.agent_name - self.handler.initial_tm_configs = validator_config - self.handler.handle(message) - log_message = self.handler.LogMessages.failed_to_parse_address.value - assert log_message in caplog.text - - def test_handle_genesis_info(self, caplog: LogCaptureFixture) -> None: - """Test handle response for genesis info with valid address""" - performative = TendermintMessage.Performative.GENESIS_INFO - info = json.dumps(self.dummy_validator_config[self.agent_name]) - message = TendermintMessage(performative, info=info) # type: ignore - self.context.agent_address = message.sender = self.agent_name - self.handler.initial_tm_configs = self.dummy_validator_config - self.handler.handle(message) - log_message = self.handler.LogMessages.collected_config_info.value - assert log_message in caplog.text - - @pytest.mark.parametrize("registered", (True, False)) - @pytest.mark.parametrize( - "performative", - ( - TendermintMessage.Performative.RECOVERY_PARAMS, - TendermintMessage.Performative.GET_RECOVERY_PARAMS, - ), - ) - def test_recovery_params( - self, - registered: bool, - performative: TendermintMessage.Performative, - caplog: LogCaptureFixture, - ) -> None: - """Test handle response for recovery parameters.""" - if not registered: - self.agent_name = "not-registered" - - if performative == TendermintMessage.Performative.GET_RECOVERY_PARAMS: - self.context.state.tm_recovery_params = TendermintRecoveryParams( - "DummyRound" - ) - message = TendermintMessage(performative) # type: ignore - log_message = self.handler.LogMessages.sending_request_response.value - elif performative == TendermintMessage.Performative.RECOVERY_PARAMS: - params = json.dumps( - asdict(TendermintRecoveryParams(DummyRound.auto_round_id())) - ) - message = TendermintMessage(performative, params=params) # type: ignore - log_message = self.handler.LogMessages.collected_params.value - else: - raise AssertionError( - f"Invalid performative {performative} for `test_recovery_params`." - ) - - self.context.agent_address = message.sender = self.agent_name - tm_configs = {self.agent_name: {"dummy": "value"}} if registered else {} - - self.handler.initial_tm_configs = tm_configs - self.handler.handle(message) - - if not registered: - log_message = self.handler.LogMessages.not_in_registered_addresses.value - - assert log_message in caplog.text - - @pytest.mark.parametrize( - "side_effect, expected_exception", - ( - ( - json.decoder.JSONDecodeError("", "", 0), - ": line 1 column 1 (char 0)", - ), - ( - {"not a dict"}, - "argument after ** must be a mapping, not str", - ), - ), - ) - def test_recovery_params_error( - self, - side_effect: Any, - expected_exception: str, - caplog: LogCaptureFixture, - ) -> None: - """Test handle response for recovery parameters.""" - message = TendermintMessage( - TendermintMessage.Performative.RECOVERY_PARAMS, params=MagicMock() # type: ignore - ) - - self.context.agent_address = message.sender = self.agent_name - tm_configs = {self.agent_name: {"dummy": "value"}} - self.handler.initial_tm_configs = tm_configs - with mock.patch.object(json, "loads", side_effect=side_effect): - self.handler.handle(message) - - log_message = self.handler.LogMessages.failed_to_parse_params.value - assert log_message in caplog.text - assert expected_exception in caplog.text - - # error - def test_handle_error(self, caplog: LogCaptureFixture) -> None: - """Test handle error""" - message = self.make_error_message() - self.handler.initial_tm_configs = self.dummy_validator_config - self.handler.handle(message) - log_message = self.handler.LogMessages.received_error_response.value - assert log_message in caplog.text - - def test_handle_error_no_target_message_retrieved( - self, caplog: LogCaptureFixture - ) -> None: - """Test handle error no target message retrieved""" - message, nonce = self.make_error_message(), "0" - dialogue = TendermintDialogue(mock.Mock(), "Bob", mock.Mock()) - dialogue.dialogue_label.dialogue_reference = nonce, "stub" - self.handler.dialogues.update = lambda _: dialogue # type: ignore - callback = lambda *args, **kwargs: None # noqa: E731 - self.context.requests.request_id_to_callback = {nonce: callback} - self.handler.initial_tm_configs = self.dummy_validator_config - self.handler.handle(message) - log_message = ( - self.handler.LogMessages.received_error_without_target_message.value - ) - assert log_message in caplog.text - - # performative - def test_handle_performative_not_recognized( - self, caplog: LogCaptureFixture - ) -> None: - """Test performative no recognized""" - message = self.make_error_message() - message._slots.performative = MagicMock(value="wacky") - self.handler.initial_tm_configs = self.dummy_validator_config - self.handler.handle(message) - log_message = self.handler.LogMessages.performative_not_recognized.value - assert log_message in caplog.text - - def test_sender_not_in_registered_addresses( - self, caplog: LogCaptureFixture - ) -> None: - """Test sender not in registered addresses.""" - - performative = TendermintMessage.Performative.GET_GENESIS_INFO - message = TendermintMessage(performative) # type: ignore - self.context.agent_address = message.sender = "dummy" - self.handler.initial_tm_configs = self.dummy_validator_config - self.handler.handle(message) - log_message = self.handler.LogMessages.not_in_registered_addresses.value - assert log_message in caplog.text diff --git a/packages/valory/skills/abstract_round_abci/tests/test_models.py b/packages/valory/skills/abstract_round_abci/tests/test_models.py deleted file mode 100644 index ee598b7..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/test_models.py +++ /dev/null @@ -1,901 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test the models.py module of the skill.""" - -# pylint: skip-file - -import builtins -import json -import logging -import re -from collections import OrderedDict -from dataclasses import dataclass -from enum import Enum -from pathlib import Path -from tempfile import TemporaryDirectory -from time import sleep -from typing import Any, Dict, List, Optional, Set, Tuple, Type, cast -from unittest import mock -from unittest.mock import MagicMock - -import pytest -from aea.exceptions import AEAEnforceError -from typing_extensions import Literal, TypedDict - -from packages.valory.skills.abstract_round_abci.base import ( - AbstractRound, - BaseSynchronizedData, - OffenceStatus, - OffenseStatusEncoder, - ROUND_COUNT_DEFAULT, -) -from packages.valory.skills.abstract_round_abci.models import ( - ApiSpecs, - BaseParams, - BenchmarkTool, - DEFAULT_BACKOFF_FACTOR, - GenesisBlock, - GenesisConfig, - GenesisConsensusParams, - GenesisEvidence, - GenesisValidator, - MIN_RESET_PAUSE_DURATION, - NUMBER_OF_RETRIES, - Requests, -) -from packages.valory.skills.abstract_round_abci.models import ( - SharedState as BaseSharedState, -) -from packages.valory.skills.abstract_round_abci.models import ( - TendermintRecoveryParams, - _MetaSharedState, - check_type, -) -from packages.valory.skills.abstract_round_abci.test_tools.abci_app import AbciAppTest -from packages.valory.skills.abstract_round_abci.tests.conftest import ( - irrelevant_genesis_config, -) - - -BASE_DUMMY_SPECS_CONFIG = dict( - name="dummy", - skill_context=MagicMock(), - url="http://dummy", - api_id="api_id", - method="GET", - headers=OrderedDict([("Dummy-Header", "dummy_value")]), - parameters=OrderedDict([("Dummy-Param", "dummy_param")]), -) - -BASE_DUMMY_PARAMS = dict( - name="", - skill_context=MagicMock(is_abstract_component=True), - setup={}, - tendermint_url="", - max_healthcheck=1, - round_timeout_seconds=1.0, - sleep_time=1, - retry_timeout=1, - retry_attempts=1, - reset_pause_duration=MIN_RESET_PAUSE_DURATION, - drand_public_key="", - tendermint_com_url="", - reset_tendermint_after=1, - service_id="abstract_round_abci", - service_registry_address="0xa51c1fc2f0d1a1b8494ed1fe312d7c3a78ed91c0", - keeper_timeout=1.0, - tendermint_check_sleep_delay=3, - tendermint_max_retries=5, - cleanup_history_depth=0, - genesis_config=irrelevant_genesis_config, - cleanup_history_depth_current=None, - request_timeout=0.0, - request_retry_delay=0.0, - tx_timeout=0.0, - max_attempts=0, - on_chain_service_id=None, - share_tm_config_on_startup=False, - tendermint_p2p_url="", - use_termination=False, - use_slashing=False, - slash_cooldown_hours=3, - slash_threshold_amount=10_000_000_000_000_000, - light_slash_unit_amount=5_000_000_000_000_000, - serious_slash_unit_amount=8_000_000_000_000_000, -) - - -class TestApiSpecsModel: - """Test ApiSpecsModel.""" - - api_specs: ApiSpecs - - def setup( - self, - ) -> None: - """Setup test.""" - - self.api_specs = ApiSpecs( - **BASE_DUMMY_SPECS_CONFIG, - response_key="value", - response_index=0, - response_type="float", - error_key="error", - error_index=None, - error_type="str", - error_data="error text", - ) - - def test_init( - self, - ) -> None: - """Test initialization.""" - - # test ensure method. - with pytest.raises( - AEAEnforceError, - match="'url' of type '' required, but it is not set in `models.params.args` of `skill.yaml` of", - ): - _ = ApiSpecs( - name="dummy", - skill_context=MagicMock(), - ) - - assert self.api_specs.retries_info.backoff_factor == DEFAULT_BACKOFF_FACTOR - assert self.api_specs.retries_info.retries == NUMBER_OF_RETRIES - assert self.api_specs.retries_info.retries_attempted == 0 - - assert self.api_specs.url == "http://dummy" - assert self.api_specs.api_id == "api_id" - assert self.api_specs.method == "GET" - assert self.api_specs.headers == {"Dummy-Header": "dummy_value"} - assert self.api_specs.parameters == {"Dummy-Param": "dummy_param"} - assert self.api_specs.response_info.response_key == "value" - assert self.api_specs.response_info.response_index == 0 - assert self.api_specs.response_info.response_type == "float" - assert self.api_specs.response_info.error_key == "error" - assert self.api_specs.response_info.error_index is None - assert self.api_specs.response_info.error_type == "str" - assert self.api_specs.response_info.error_data is None - - @pytest.mark.parametrize("retries", range(10)) - def test_suggested_sleep_time(self, retries: int) -> None: - """Test `suggested_sleep_time`""" - self.api_specs.retries_info.retries_attempted = retries - assert ( - self.api_specs.retries_info.suggested_sleep_time - == DEFAULT_BACKOFF_FACTOR**retries - ) - - def test_retries( - self, - ) -> None: - """Tests for retries.""" - - self.api_specs.increment_retries() - assert self.api_specs.retries_info.retries_attempted == 1 - assert not self.api_specs.is_retries_exceeded() - - for _ in range(NUMBER_OF_RETRIES): - self.api_specs.increment_retries() - assert self.api_specs.is_retries_exceeded() - self.api_specs.reset_retries() - assert self.api_specs.retries_info.retries_attempted == 0 - - def test_get_spec( - self, - ) -> None: - """Test get_spec method.""" - - actual_specs = { - "url": "http://dummy", - "method": "GET", - "headers": {"Dummy-Header": "dummy_value"}, - "parameters": {"Dummy-Param": "dummy_param"}, - } - - specs = self.api_specs.get_spec() - assert all([key in specs for key in actual_specs.keys()]) - assert all([specs[key] == actual_specs[key] for key in actual_specs]) - - @pytest.mark.parametrize( - "api_specs_config, message, expected_res, expected_error", - ( - ( - dict( - **BASE_DUMMY_SPECS_CONFIG, - response_key="value", - response_index=None, - response_type="float", - error_key=None, - error_index=None, - error_data=None, - ), - MagicMock(body=b'{"value": "10.232"}'), - 10.232, - None, - ), - ( - dict( - **BASE_DUMMY_SPECS_CONFIG, - response_key="test:response:key", - response_index=2, - response_type="dict", - error_key="error:key", - error_index=3, - error_type="str", - error_data=None, - ), - MagicMock( - body=b'{"test": {"response": {"key": ["does_not_matter", "does_not_matter", {"this": "matters"}]}}}' - ), - {"this": "matters"}, - None, - ), - ( - dict( - **BASE_DUMMY_SPECS_CONFIG, - response_key="test:response:key", - response_index=2, - error_key="error:key", - error_index=3, - error_type="str", - error_data=None, - ), - MagicMock(body=b'{"cannot be parsed'), - None, - None, - ), - ( - dict( - **BASE_DUMMY_SPECS_CONFIG, - response_key="test:response:key", - response_index=2, - error_key="error:key", - error_index=3, - error_type="str", - error_data=None, - ), - MagicMock( - # the null will raise `TypeError` and we test that it is handled - body=b'{"test": {"response": {"key": ["does_not_matter", "does_not_matter", null]}}}' - ), - "None", - None, - ), - ( - dict( - **BASE_DUMMY_SPECS_CONFIG, - response_key="test:response:key", - response_index=2, # this will raise `IndexError` and we test that it is handled - error_key="error:key", - error_index=3, - error_type="str", - error_data=None, - ), - MagicMock( - body=b'{"test": {"response": {"key": ["does_not_matter", "does_not_matter"]}}}' - ), - None, - None, - ), - ( - dict( - **BASE_DUMMY_SPECS_CONFIG, - response_key="test:response:key", # this will raise `KeyError` and we test that it is handled - response_index=2, - error_key="error:key", - error_index=3, - error_type="str", - error_data=None, - ), - MagicMock( - body=b'{"test": {"response": {"key_does_not_match": ["does_not_matter", "does_not_matter"]}}}' - ), - None, - None, - ), - ( - dict( - **BASE_DUMMY_SPECS_CONFIG, - response_key="test:response:key", - response_index=2, - error_key="error:key", - error_index=3, - error_type="str", - error_data=None, - ), - MagicMock( - body=b'{"test": {"response": {"key_does_not_match": ["does_not_matter", "does_not_matter"]}}, ' - b'"error": {"key": [0, 1, 2, "test that the error is being parsed correctly"]}}' - ), - None, - "test that the error is being parsed correctly", - ), - ), - ) - def test_process_response( - self, - api_specs_config: dict, - message: MagicMock, - expected_res: Any, - expected_error: Any, - ) -> None: - """Test `process_response` method.""" - api_specs = ApiSpecs(**api_specs_config) - actual = api_specs.process_response(message) - assert actual == expected_res - response_type = api_specs_config.get("response_type", None) - if response_type is not None: - assert type(actual) == getattr(builtins, response_type) - assert api_specs.response_info.error_data == expected_error - - def test_attribute_manipulation(self) -> None: - """Test manipulating the attributes.""" - with pytest.raises(AttributeError, match="This object is frozen!"): - del self.api_specs.url - - with pytest.raises(AttributeError, match="This object is frozen!"): - self.api_specs.url = "" - - self.api_specs.__dict__["_frozen"] = False - self.api_specs.url = "" - del self.api_specs.url - - -class ConcreteRound(AbstractRound): - """A ConcreteRoundA for testing purposes.""" - - synchronized_data_class = MagicMock() - payload_attribute = MagicMock() - payload_class = MagicMock() - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: - """Handle the end of the block.""" - - -class SharedState(BaseSharedState): - """Shared State for testing purposes.""" - - abci_app_cls = AbciAppTest - - -class TestSharedState: - """Test SharedState(Model) class.""" - - def test_initialization(self, *_: Any) -> None: - """Test the initialization of the shared state.""" - SharedState(name="", skill_context=MagicMock()) - - @staticmethod - def dummy_state_setup(shared_state: SharedState) -> None: - """Setup a shared state instance with dummy params.""" - shared_state.context.params.setup_params = { - "test": [], - "all_participants": list(range(4)), - } - shared_state.setup() - - @pytest.mark.parametrize( - "acn_configured_agents, validator_to_agent, raises", - ( - ( - {i for i in range(4)}, - {f"validator_address_{i}": i for i in range(4)}, - False, - ), - ( - {i for i in range(5)}, - {f"validator_address_{i}": i for i in range(4)}, - True, - ), - ( - {i for i in range(4)}, - {f"validator_address_{i}": i for i in range(5)}, - True, - ), - ), - ) - def test_setup_slashing( - self, - acn_configured_agents: Set[str], - validator_to_agent: Dict[str, str], - raises: bool, - ) -> None: - """Test the `validator_to_agent` properties.""" - shared_state = SharedState(name="", skill_context=MagicMock()) - self.dummy_state_setup(shared_state) - - if not raises: - shared_state.initial_tm_configs = dict.fromkeys(acn_configured_agents) - shared_state.setup_slashing(validator_to_agent) - assert shared_state.round_sequence.validator_to_agent == validator_to_agent - - status = shared_state.round_sequence.offence_status - encoded_status = json.dumps( - status, - cls=OffenseStatusEncoder, - ) - expected_status = { - agent: OffenceStatus() for agent in acn_configured_agents - } - encoded_expected_status = json.dumps( - expected_status, cls=OffenseStatusEncoder - ) - - assert encoded_status == encoded_expected_status - - random_agent = acn_configured_agents.pop() - status[random_agent].num_unknown_offenses = 10 - assert status[random_agent].num_unknown_offenses == 10 - - for other_agent in acn_configured_agents - {random_agent}: - assert status[other_agent].num_unknown_offenses == 0 - - return - - expected_diff = acn_configured_agents.symmetric_difference( - validator_to_agent.values() - ) - with pytest.raises( - ValueError, - match=re.escape( - f"Trying to use the mapping `{validator_to_agent}`, which contains validators for non-configured " - "agents and/or does not contain validators for some configured agents. The agents which have been " - f"configured via ACN are `{acn_configured_agents}` and the diff was for {expected_diff}." - ), - ): - shared_state.initial_tm_configs = dict.fromkeys(acn_configured_agents) - shared_state.setup_slashing(validator_to_agent) - - def test_setup(self, *_: Any) -> None: - """Test setup method.""" - shared_state = SharedState( - name="", skill_context=MagicMock(is_abstract_component=False) - ) - assert shared_state.initial_tm_configs == {} - self.dummy_state_setup(shared_state) - assert shared_state.initial_tm_configs == {i: None for i in range(4)} - - @pytest.mark.parametrize( - "initial_tm_configs, address_input, exception, expected", - ( - ( - {}, - "0x1", - "The validator address of non-participating agent `0x1` was requested.", - None, - ), - ({}, "0x0", "SharedState's setup was not performed successfully.", None), - ( - {"0x0": None}, - "0x0", - "ACN registration has not been successfully performed for agent `0x0`. " - "Have you set the `share_tm_config_on_startup` flag to `true` in the configuration?", - None, - ), - ( - {"0x0": {}}, - "0x0", - "The tendermint configuration for agent `0x0` is invalid: `{}`.", - None, - ), - ( - {"0x0": {"address": None}}, - "0x0", - "The tendermint configuration for agent `0x0` is invalid: `{'address': None}`.", - None, - ), - ( - {"0x0": {"address": "test_validator_address"}}, - "0x0", - None, - "test_validator_address", - ), - ), - ) - def test_get_validator_address( - self, - initial_tm_configs: Dict[str, Optional[Dict[str, Any]]], - address_input: str, - exception: Optional[str], - expected: Optional[str], - ) -> None: - """Test `get_validator_address` method.""" - shared_state = SharedState(name="", skill_context=MagicMock()) - with mock.patch.object(shared_state.context, "params") as mock_params: - mock_params.setup_params = { - "all_participants": ["0x0"], - } - shared_state.setup() - shared_state.initial_tm_configs = initial_tm_configs - if exception is None: - assert shared_state.get_validator_address(address_input) == expected - return - with pytest.raises(ValueError, match=exception): - shared_state.get_validator_address(address_input) - - @pytest.mark.parametrize("self_idx", (range(4))) - def test_acn_container(self, self_idx: int) -> None: - """Test the `acn_container` method.""" - - shared_state = SharedState( - name="", skill_context=MagicMock(agent_address=self_idx) - ) - self.dummy_state_setup(shared_state) - expected = {i: None for i in range(4) if i != self_idx} - assert shared_state.acn_container() == expected - - def test_synchronized_data_negative_not_available(self, *_: Any) -> None: - """Test 'synchronized_data' property getter, negative case (not available).""" - shared_state = SharedState(name="", skill_context=MagicMock()) - with pytest.raises(ValueError, match="round sequence not available"): - shared_state.synchronized_data - - def test_synchronized_data_positive(self, *_: Any) -> None: - """Test 'synchronized_data' property getter, negative case (not available).""" - shared_state = SharedState(name="", skill_context=MagicMock()) - shared_state.context.params.setup_params = { - "test": [], - "all_participants": [["0x0"]], - } - shared_state.setup() - shared_state.round_sequence.abci_app._round_results = [MagicMock()] - shared_state.synchronized_data - - def test_synchronized_data_db(self, *_: Any) -> None: - """Test 'synchronized_data' AbciAppDB.""" - shared_state = SharedState(name="", skill_context=MagicMock()) - with mock.patch.object(shared_state.context, "params") as mock_params: - mock_params.setup_params = { - "safe_contract_address": "0xsafe", - "oracle_contract_address": "0xoracle", - "all_participants": "0x0", - } - shared_state.setup() - for key, value in mock_params.setup_params.items(): - assert shared_state.synchronized_data.db.get_strict(key) == value - - @pytest.mark.parametrize( - "address_to_acn_deliverable, n_participants, expected", - ( - ({}, 4, None), - ({i: "test" for i in range(4)}, 4, "test"), - ( - {i: TendermintRecoveryParams("test") for i in range(4)}, - 4, - TendermintRecoveryParams("test"), - ), - ({1: "test", 2: "non-matching", 3: "test", 4: "test"}, 4, "test"), - ({i: "test" for i in range(4)}, 4, "test"), - ({1: "no", 2: "result", 3: "matches", 4: ""}, 4, None), - ), - ) - def test_get_acn_result( - self, - address_to_acn_deliverable: Dict[str, Any], - n_participants: int, - expected: Optional[str], - ) -> None: - """Test `get_acn_result`.""" - shared_state = SharedState( - abci_app_cls=AbciAppTest, name="", skill_context=MagicMock() - ) - shared_state.context.params.setup_params = { - "test": [], - "all_participants": ["0x0"], - } - shared_state.setup() - shared_state.synchronized_data.update(participants=tuple(range(n_participants))) - shared_state.address_to_acn_deliverable = address_to_acn_deliverable - actual = shared_state.get_acn_result() - - assert actual == expected - - def test_recovery_params_on_init(self) -> None: - """Test that `tm_recovery_params` get initialized correctly.""" - shared_state = SharedState(name="", skill_context=MagicMock()) - assert shared_state.tm_recovery_params is not None - assert shared_state.tm_recovery_params.round_count == ROUND_COUNT_DEFAULT - assert ( - shared_state.tm_recovery_params.reset_from_round - == AbciAppTest.initial_round_cls.auto_round_id() - ) - assert shared_state.tm_recovery_params.reset_params is None - - def test_set_last_reset_params(self) -> None: - """Test that `last_reset_params` get set correctly.""" - shared_state = SharedState(name="", skill_context=MagicMock()) - test_params = [("genesis_time", "some-time"), ("initial_height", "0")] - shared_state.last_reset_params = test_params - assert shared_state.last_reset_params == test_params - - -class TestBenchmarkTool: - """Test BenchmarkTool""" - - @staticmethod - def _check_behaviour_data(data: List, agent_name: str) -> None: - """Check behaviour data.""" - assert len(data) == 1 - - (behaviour_data,) = data - assert behaviour_data["behaviour"] == agent_name - assert all( - [key in behaviour_data["data"] for key in ("local", "consensus", "total")] - ) - - def test_end_2_end(self) -> None: - """Test end 2 end of the tool.""" - - agent_name = "agent" - skill_context = MagicMock( - agent_address=agent_name, logger=MagicMock(info=logging.info) - ) - - with TemporaryDirectory() as temp_dir: - benchmark = BenchmarkTool( - name=agent_name, skill_context=skill_context, log_dir=temp_dir - ) - - with benchmark.measure(agent_name).local(): - sleep(1.0) - - with benchmark.measure(agent_name).consensus(): - sleep(1.0) - - self._check_behaviour_data(benchmark.data, agent_name) - - benchmark.save() - - benchmark_dir = Path(temp_dir, agent_name) - benchmark_file = benchmark_dir / "0.json" - assert (benchmark_file).is_file() - - behaviour_data = json.loads(benchmark_file.read_text()) - self._check_behaviour_data(behaviour_data, agent_name) - - -def test_requests_model_initialization() -> None: - """Test initialization of the 'Requests(Model)' class.""" - Requests(name="", skill_context=MagicMock()) - - -def test_base_params_model_initialization() -> None: - """Test initialization of the 'BaseParams(Model)' class.""" - kwargs = BASE_DUMMY_PARAMS.copy() - bp = BaseParams(**kwargs) - - with pytest.raises(AttributeError, match="This object is frozen!"): - bp.request_timeout = 0.1 - - with pytest.raises(AttributeError, match="This object is frozen!"): - del bp.request_timeout - - bp.__dict__["_frozen"] = False - del bp.request_timeout - - assert getattr(bp, "request_timeout", None) is None - - kwargs["skill_context"] = MagicMock(is_abstract_component=False) - required_setup_params = { - "safe_contract_address": "0x0", - "all_participants": ["0x0"], - "consensus_threshold": 1, - } - kwargs["setup"] = required_setup_params - BaseParams(**kwargs) - - -@pytest.mark.parametrize( - "setup, error_text", - ( - ({}, "`setup` params contain no values!"), - ( - {"a": "b"}, - "Value for `safe_contract_address` missing from the `setup` params.", - ), - ), -) -def test_incorrect_setup(setup: Dict[str, Any], error_text: str) -> None: - """Test BaseParams model initialization with incorrect setup data.""" - kwargs = BASE_DUMMY_PARAMS.copy() - - with pytest.raises( - AEAEnforceError, - match=error_text, - ): - kwargs["skill_context"] = MagicMock(is_abstract_component=False) - kwargs["setup"] = setup - BaseParams(**kwargs) - - with pytest.raises( - AEAEnforceError, - match=f"`reset_pause_duration` must be greater than or equal to {MIN_RESET_PAUSE_DURATION}", - ): - kwargs["reset_pause_duration"] = MIN_RESET_PAUSE_DURATION - 1 - BaseParams(**kwargs) - - -def test_genesis_block() -> None: - """Test genesis block methods.""" - json = {"max_bytes": "a", "max_gas": "b", "time_iota_ms": "c"} - gb = GenesisBlock(**json) - assert gb.to_json() == json - - with pytest.raises(TypeError, match="Error in field 'max_bytes'. Expected type .*"): - json["max_bytes"] = 0 # type: ignore - GenesisBlock(**json) - - -def test_genesis_evidence() -> None: - """Test genesis evidence methods.""" - json = {"max_age_num_blocks": "a", "max_age_duration": "b", "max_bytes": "c"} - ge = GenesisEvidence(**json) - assert ge.to_json() == json - - -def test_genesis_validator() -> None: - """Test genesis validator methods.""" - json = {"pub_key_types": ["a", "b"]} - ge = GenesisValidator(pub_key_types=tuple(json["pub_key_types"])) - assert ge.to_json() == json - - with pytest.raises( - TypeError, match="Error in field 'pub_key_types'. Expected type .*" - ): - GenesisValidator(**json) # type: ignore - - -def test_genesis_consensus_params() -> None: - """Test genesis consensus params methods.""" - consensus_params = cast(Dict, irrelevant_genesis_config["consensus_params"]) - gcp = GenesisConsensusParams.from_json_dict(consensus_params) - assert gcp.to_json() == consensus_params - - -def test_genesis_config() -> None: - """Test genesis config methods.""" - gcp = GenesisConfig.from_json_dict(irrelevant_genesis_config) - assert gcp.to_json() == irrelevant_genesis_config - - -def test_meta_shared_state_when_instance_not_subclass_of_shared_state() -> None: - """Test instantiation of meta class when instance not a subclass of shared state.""" - - class MySharedState(metaclass=_MetaSharedState): - pass - - -def test_shared_state_instantiation_without_attributes_raises_error() -> None: - """Test that definition of concrete subclass of SharedState without attributes raises error.""" - with pytest.raises(AttributeError, match="'abci_app_cls' not set on .*"): - - class MySharedState(BaseSharedState): - pass - - with pytest.raises(AttributeError, match="The object `None` is not a class"): - - class MySharedStateB(BaseSharedState): - abci_app_cls = None # type: ignore - - with pytest.raises( - AttributeError, - match="The class is not an instance of packages.valory.skills.abstract_round_abci.base.AbciApp", - ): - - class MySharedStateC(BaseSharedState): - abci_app_cls = MagicMock - - -@dataclass -class A: - """Class for testing.""" - - value: int - - -@dataclass -class B: - """Class for testing.""" - - value: str - - -class C(TypedDict): - """Class for testing.""" - - name: str - year: int - - -class D(TypedDict, total=False): - """Class for testing.""" - - name: str - year: int - - -testdata_positive = [ - ("test_arg", 1, int), - ("test_arg", "1", str), - ("test_arg", True, bool), - ("test_arg", 1, Optional[int]), - ("test_arg", None, Optional[int]), - ("test_arg", "1", Optional[str]), - ("test_arg", None, Optional[str]), - ("test_arg", None, Optional[bool]), - ("test_arg", None, Optional[List[int]]), - ("test_arg", [], Optional[List[int]]), - ("test_arg", [1], Optional[List[int]]), - ("test_arg", {"str": 1}, Optional[Dict[str, int]]), - ("test_arg", {"str": A(1)}, Dict[str, A]), - ("test_arg", [("1", "2")], List[Tuple[str, str]]), - ("test_arg", [1], List[Optional[int]]), - ("test_arg", [1, None], List[Optional[int]]), - ("test_arg", A, Type[A]), - ("test_arg", A, Optional[Type[A]]), - ("test_arg", None, Optional[Type[A]]), - ("test_arg", MagicMock(), Optional[Type[A]]), # any type allowed - ("test_arg", {"name": "str", "year": 1}, C), - ("test_arg", 42, Literal[42]), - ("test_arg", {"name": "str"}, D), -] - - -@pytest.mark.parametrize("name,value,type_hint", testdata_positive) -def test_type_check_positive(name: str, value: Any, type_hint: Any) -> None: - """Test the type check mixin.""" - - check_type(name, value, type_hint) - - -testdata_negative = [ - ("test_arg", "1", int), - ("test_arg", 1, str), - ("test_arg", None, bool), - ("test_arg", "1", Optional[int]), - ("test_arg", 1, Optional[str]), - ("test_arg", 1, Optional[bool]), - ("test_arg", ["1"], Optional[List[int]]), - ("test_arg", {"str": "1"}, Optional[Dict[str, int]]), - ("test_arg", {1: 1}, Optional[Dict[str, int]]), - ("test_arg", {"str": B("1")}, Dict[str, A]), - ("test_arg", [()], List[Tuple[str, str]]), - ("test_arg", [("1",)], List[Tuple[str, str]]), - ("test_arg", [("1", 1)], List[Tuple[str, str]]), - ("test_arg", [("1", 1, "1")], List[Tuple[str, ...]]), - ("test_arg", ["1"], List[Optional[int]]), - ("test_arg", [1, None, "1"], List[Optional[int]]), - ("test_arg", B, Type[A]), - ("test_arg", B, Optional[Type[A]]), - ("test_arg", {"name": "str", "year": "1"}, C), - ("test_arg", 41, Literal[42]), - ("test_arg", C({"name": "str", "year": 1}), A), - ("test_arg", {"name": "str"}, C), -] - - -@pytest.mark.parametrize("name,value,type_hint", testdata_negative) -def test_type_check_negative(name: str, value: Any, type_hint: Any) -> None: - """Test the type check mixin.""" - - with pytest.raises(TypeError): - check_type(name, value, type_hint) diff --git a/packages/valory/skills/abstract_round_abci/tests/test_utils.py b/packages/valory/skills/abstract_round_abci/tests/test_utils.py deleted file mode 100644 index 3f41273..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/test_utils.py +++ /dev/null @@ -1,307 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test the utils.py module of the skill.""" - -from collections import defaultdict -from string import printable -from typing import Any, Dict, List, Tuple, Type -from unittest import mock - -import pytest -from hypothesis import assume, given, settings -from hypothesis import strategies as st - -from packages.valory.skills.abstract_round_abci.tests.conftest import profile_name -from packages.valory.skills.abstract_round_abci.utils import ( - DEFAULT_TENDERMINT_P2P_PORT, - KeyType, - MAX_UINT64, - ValueType, - VerifyDrand, - consensus_threshold, - filter_negative, - get_data_from_nested_dict, - get_value_with_type, - inverse, - is_json_serializable, - is_primitive_or_none, - parse_tendermint_p2p_url, -) - - -settings.load_profile(profile_name) - - -# pylint: skip-file - - -DRAND_PUBLIC_KEY: str = "868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31" - -DRAND_VALUE = { - "round": 1416669, - "randomness": "f6be4bf1fa229f22340c1a5b258f809ac4af558200775a67dacb05f0cb258a11", - "signature": ( - "b44d00516f46da3a503f9559a634869b6dc2e5d839e46ec61a090e3032172954929a5" - "d9bd7197d7739fe55db770543c71182562bd0ad20922eb4fe6b8a1062ed21df3b68de" - "44694eb4f20b35262fa9d63aa80ad3f6172dd4d33a663f21179604" - ), - "previous_signature": ( - "903c60a4b937a804001032499a855025573040cb86017c38e2b1c3725286756ce8f33" - "61188789c17336beaf3f9dbf84b0ad3c86add187987a9a0685bc5a303e37b008fba8c" - "44f02a416480dd117a3ff8b8075b1b7362c58af195573623187463" - ), -} - - -class TestVerifyDrand: - """Test DrandVerify.""" - - drand_check: VerifyDrand - - def setup( - self, - ) -> None: - """Setup test.""" - self.drand_check = VerifyDrand() - - def test_verify( - self, - ) -> None: - """Test verify method.""" - - result, error = self.drand_check.verify(DRAND_VALUE, DRAND_PUBLIC_KEY) - assert result - assert error is None - - def test_verify_fails( - self, - ) -> None: - """Test verify method.""" - - drand_value = DRAND_VALUE.copy() - del drand_value["randomness"] - result, error = self.drand_check.verify(drand_value, DRAND_PUBLIC_KEY) - assert not result - assert error == "DRAND dict is missing value for 'randomness'" - - drand_value = DRAND_VALUE.copy() - drand_value["randomness"] = "".join( - list(drand_value["randomness"])[:-1] + ["0"] # type: ignore - ) - result, error = self.drand_check.verify(drand_value, DRAND_PUBLIC_KEY) - assert not result - assert error == "Failed randomness hash check." - - drand_value = DRAND_VALUE.copy() - with mock.patch.object( - self.drand_check, "_verify_signature", return_value=False - ): - result, error = self.drand_check.verify(drand_value, DRAND_PUBLIC_KEY) - - assert not result - assert error == "Failed bls.Verify check." - - @pytest.mark.parametrize("value", (-1, MAX_UINT64 + 1)) - def test_negative_and_overflow(self, value: int) -> None: - """Test verify method.""" - with pytest.raises(ValueError): - self.drand_check._int_to_bytes_big(value) - - -@given(st.integers(min_value=0, max_value=MAX_UINT64)) -def test_verify_int_to_bytes_big_fuzz(integer: int) -> None: - """Test VerifyDrand.""" - - VerifyDrand._int_to_bytes_big(integer) - - -@pytest.mark.parametrize("integer", [-1, MAX_UINT64 + 1]) -def test_verify_int_to_bytes_big_raises(integer: int) -> None: - """Test VerifyDrand._int_to_bytes_big""" - - expected = "VerifyDrand can only handle positive numbers representable with 8 bytes" - with pytest.raises(ValueError, match=expected): - VerifyDrand._int_to_bytes_big(integer) - - -@given(st.binary()) -def test_verify_randomness_hash_fuzz(input_bytes: bytes) -> None: - """Test VerifyDrand._verify_randomness_hash""" - - VerifyDrand._verify_randomness_hash(input_bytes, input_bytes) - - -@given( - st.lists(st.text(), min_size=1, max_size=50), - st.binary(), - st.characters(), -) -def test_get_data_from_nested_dict( - nested_keys: List[str], final_value: bytes, separator: str -) -> None: - """Test `get_data_from_nested_dict`""" - assume(not any(separator in key for key in nested_keys)) - - def create_nested_dict() -> defaultdict: - """Recursively create a nested dict of arbitrary size.""" - return defaultdict(create_nested_dict) - - nested_dict = create_nested_dict() - key_access = (f"[nested_keys[{i}]]" for i in range(len(nested_keys))) - expression = "nested_dict" + "".join(key_access) - expression += " = final_value" - exec(expression) # nosec - - serialized_keys = separator.join(nested_keys) - actual = get_data_from_nested_dict(nested_dict, serialized_keys, separator) - assert actual == final_value - - -@pytest.mark.parametrize( - "type_name, type_, value", - ( - ("str", str, "1"), - ("int", int, 1), - ("float", float, 1.1), - ("dict", dict, {1: 1}), - ("list", list, [1]), - ("non_existent", None, 1), - ), -) -def test_get_value_with_type(type_name: str, type_: Type, value: Any) -> None: - """Test `get_value_with_type`""" - if type_ is None: - with pytest.raises( - AttributeError, match=f"module 'builtins' has no attribute '{type_name}'" - ): - get_value_with_type(value, type_name) - return - - actual = get_value_with_type(value, type_name) - assert type(actual) == type_ - assert actual == value - - -@pytest.mark.parametrize( - ("url", "expected_output"), - ( - ("localhost", ("localhost", DEFAULT_TENDERMINT_P2P_PORT)), - ("localhost:80", ("localhost", 80)), - ("some.random.host:80", ("some.random.host", 80)), - ("1.1.1.1", ("1.1.1.1", DEFAULT_TENDERMINT_P2P_PORT)), - ("1.1.1.1:80", ("1.1.1.1", 80)), - ), -) -def test_parse_tendermint_p2p_url(url: str, expected_output: Tuple[str, int]) -> None: - """Test `parse_tendermint_p2p_url` method.""" - - assert parse_tendermint_p2p_url(url=url) == expected_output - - -@given( - st.one_of(st.none(), st.integers(), st.floats(), st.text(), st.booleans()), - st.one_of( - st.nothing(), - st.frozensets(st.integers()), - st.sets(st.integers()), - st.lists(st.integers()), - st.dictionaries(st.integers(), st.integers()), - st.dates(), - st.complex_numbers(), - st.just(object()), - ), -) -def test_is_primitive_or_none(valid_obj: Any, invalid_obj: Any) -> None: - """Test `is_primitive_or_none`.""" - assert is_primitive_or_none(valid_obj) - assert not is_primitive_or_none(invalid_obj) - - -@given( - st.recursive( - st.none() | st.booleans() | st.floats() | st.text(printable), - lambda children: st.lists(children) - | st.dictionaries(st.text(printable), children), - ), - st.one_of( - st.nothing(), - st.frozensets(st.integers()), - st.sets(st.integers()), - st.dates(), - st.complex_numbers(), - st.just(object()), - ), -) -def test_is_json_serializable(valid_obj: Any, invalid_obj: Any) -> None: - """Test `is_json_serializable`.""" - assert is_json_serializable(valid_obj) - assert not is_json_serializable(invalid_obj) - - -@given( - positive=st.dictionaries(st.text(), st.integers(min_value=0)), - negative=st.dictionaries(st.text(), st.integers(max_value=-1)), -) -def test_filter_negative(positive: Dict[str, int], negative: Dict[str, int]) -> None: - """Test `filter_negative`.""" - assert len(tuple(filter_negative(positive))) == 0 - assert set(filter_negative(negative)) == set(negative.keys()) - - -@pytest.mark.parametrize( - "nb, threshold", - ((1, 1), (2, 2), (3, 3), (4, 3), (5, 4), (6, 5), (100, 67), (300, 201)), -) -def test_consensus_threshold(nb: int, threshold: int) -> None: - """Test `consensus_threshold`.""" - assert consensus_threshold(nb) == threshold - - -@pytest.mark.parametrize( - "dict_, expected", - ( - ({}, {}), - ( - {"test": "this", "which?": "this"}, - {"this": ["test", "which?"]}, - ), - ( - {"test": "this", "which?": "this", "hm": "ok"}, - {"this": ["test", "which?"], "ok": ["hm"]}, - ), - ( - {"test": "this", "hm": "ok"}, - {"this": ["test"], "ok": ["hm"]}, - ), - ( - {"test": "this", "hm": "ok", "ok": "ok"}, - {"this": ["test"], "ok": ["hm", "ok"]}, - ), - ( - {"test": "this", "which?": "this", "hm": "ok", "ok": "ok"}, - {"this": ["test", "which?"], "ok": ["hm", "ok"]}, - ), - ), -) -def test_inverse( - dict_: Dict[KeyType, ValueType], expected: Dict[ValueType, List[KeyType]] -) -> None: - """Test `inverse`.""" - assert inverse(dict_) == expected diff --git a/packages/valory/skills/abstract_round_abci/utils.py b/packages/valory/skills/abstract_round_abci/utils.py deleted file mode 100644 index 864658c..0000000 --- a/packages/valory/skills/abstract_round_abci/utils.py +++ /dev/null @@ -1,504 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains utility functions for the 'abstract_round_abci' skill.""" - -import builtins -import collections -import dataclasses -import sys -import types -import typing -from hashlib import sha256 -from math import ceil -from typing import ( - Any, - Dict, - FrozenSet, - Iterator, - List, - Optional, - Set, - Tuple, - Type, - TypeVar, - Union, - cast, -) -from unittest.mock import MagicMock - -import typing_extensions -from eth_typing.bls import BLSPubkey, BLSSignature -from py_ecc.bls import G2Basic as bls -from typing_extensions import Literal, TypeGuard, TypedDict - - -MAX_UINT64 = 2**64 - 1 -DEFAULT_TENDERMINT_P2P_PORT = 26656 - - -class VerifyDrand: # pylint: disable=too-few-public-methods - """ - Tool to verify Randomness retrieved from various external APIs. - - The ciphersuite used is BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_ - - cryptographic-specification section in https://drand.love/docs/specification/ - https://github.com/ethereum/py_ecc - """ - - @classmethod - def _int_to_bytes_big(cls, value: int) -> bytes: - """Convert int to bytes.""" - if value < 0 or value > MAX_UINT64: - raise ValueError( - "VerifyDrand can only handle positive numbers representable with 8 bytes" - ) - return int.to_bytes(value, 8, byteorder="big", signed=False) - - @classmethod - def _verify_randomness_hash(cls, randomness: bytes, signature: bytes) -> bool: - """Verify randomness hash.""" - return sha256(signature).digest() == randomness - - @classmethod - def _verify_signature( - cls, - pubkey: Union[BLSPubkey, bytes], - message: bytes, - signature: Union[BLSSignature, bytes], - ) -> bool: - """Verify randomness signature.""" - return bls.Verify( - cast(BLSPubkey, pubkey), message, cast(BLSSignature, signature) - ) - - def verify(self, data: Dict, pubkey: str) -> Tuple[bool, Optional[str]]: - """ - Verify drand value retried from external APIs. - - :param data: dictionary containing drand parameters. - :param pubkey: league of entropy public key - public-endpoints section in https://drand.love/developer/http-api/ - :returns: bool, error message - """ - - encoded_pubkey = bytes.fromhex(pubkey) - try: - randomness = data["randomness"] - signature = data["signature"] - round_value = int(data["round"]) - except KeyError as e: - return False, f"DRAND dict is missing value for {e}" - - previous_signature = data.pop("previous_signature", "") - encoded_randomness = bytes.fromhex(randomness) - encoded_signature = bytes.fromhex(signature) - int_encoded_round = self._int_to_bytes_big(round_value) - encoded_previous_signature = bytes.fromhex(previous_signature) - - if not self._verify_randomness_hash(encoded_randomness, encoded_signature): - return False, "Failed randomness hash check." - - msg_b = encoded_previous_signature + int_encoded_round - msg_hash_b = sha256(msg_b).digest() - - if not self._verify_signature(encoded_pubkey, msg_hash_b, encoded_signature): - return False, "Failed bls.Verify check." - - return True, None - - -def get_data_from_nested_dict( - nested_dict: Dict, keys: str, separator: str = ":" -) -> Any: - """Gets content from a nested dictionary, using serialized response keys which are split by a given separator. - - :param nested_dict: the nested dictionary to get the content from - :param keys: the keys to use on the nested dictionary in order to get the content - :param separator: the separator to use in order to get the keys list. - Choose the separator carefully, so that it does not conflict with any character of the keys. - - :returns: the content result - """ - parsed_keys = keys.split(separator) - for key in parsed_keys: - nested_dict = nested_dict[key] - return nested_dict - - -def get_value_with_type(value: Any, type_name: str) -> Any: - """Get the given value as the specified type.""" - return getattr(builtins, type_name)(value) - - -def parse_tendermint_p2p_url(url: str) -> Tuple[str, int]: - """Parse tendermint P2P url.""" - hostname, *_port = url.split(":") - if len(_port) > 0: - port_str, *_ = _port - port = int(port_str) - else: - port = DEFAULT_TENDERMINT_P2P_PORT - - return hostname, port - - -## -# Typing utils - to be extracted to open-aea -## - - -try: - # Python >=3.8 should have these functions already - from typing import get_args as _get_args # pylint: disable=ungrouped-imports - from typing import get_origin as _get_origin # pylint: disable=ungrouped-imports -except ImportError: # pragma: nocover - # Python 3.7 - def _get_origin(tp): # type: ignore - """Copied from the Python 3.8 typing module""" - if isinstance(tp, typing._GenericAlias): # pylint: disable=protected-access - return tp.__origin__ - if tp is typing.Generic: - return typing.Generic - return None - - def _get_args(tp): # type: ignore - """Copied from the Python 3.8 typing module""" - if isinstance(tp, typing._GenericAlias): # pylint: disable=protected-access - res = tp.__args__ - if get_origin(tp) is collections.abc.Callable and res[0] is not Ellipsis: - res = (list(res[:-1]), res[-1]) - return res - return () - - -def get_origin(tp): # type: ignore - """ - Get the unsubscripted version of a type. - - This supports generic types, Callable, Tuple, Union, Literal, Final and - ClassVar. Returns None for unsupported types. - Examples: - get_origin(Literal[42]) is Literal - get_origin(int) is None - get_origin(ClassVar[int]) is ClassVar - get_origin(Generic) is Generic - get_origin(Generic[T]) is Generic - get_origin(Union[T, int]) is Union - get_origin(List[Tuple[T, T]][int]) == list - """ - return _get_origin(tp) - - -def get_args(tp): # type: ignore - """ - Get type arguments with all substitutions performed. - - For unions, basic simplifications used by Union constructor are performed. - Examples: - get_args(Dict[str, int]) == (str, int) - get_args(int) == () - get_args(Union[int, Union[T, int], str][int]) == (int, str) - get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int]) - get_args(Callable[[], T][int]) == ([], int) - """ - return _get_args(tp) - - -## -# The following is borrowed from https://github.com/tamuhey/dataclass_utils/blob/81580d2c0c285081db06be02b4ecdd125532bef5/dataclass_utils/type_checker.py#L152 -## - - -def is_pep604_union(ty: Type[Any]) -> bool: - """Check if a type is a PEP 604 union.""" - return sys.version_info >= (3, 10) and ty is types.UnionType # type: ignore # noqa: E721 # pylint: disable=no-member - - -def _path_to_str(path: List[str]) -> str: - """Convert a path to a string.""" - return " -> ".join(reversed(path)) - - -class AutonomyTypeError(TypeError): - """Type Error for the Autonomy type check system.""" - - def __init__( - self, - ty: Type[Any], - value: Any, - path: Optional[List[str]] = None, - ): - """Initialize AutonomyTypeError.""" - self.ty = ty - self.value = value - self.path = path or [] - super().__init__() - - def __str__(self) -> str: - """Get string representation of AutonomyTypeError.""" - path = _path_to_str(self.path) - msg = f"Error in field '{path}'. Expected type {self.ty}, got {type(self.value)} (value: {self.value})" - return msg - - -Result = Optional[AutonomyTypeError] # returns error context - - -def check( # pylint: disable=too-many-return-statements - value: Any, ty: Type[Any] -) -> Result: - """ - Check a value against a type. - - # Examples - >>> assert is_error(check(1, str)) - >>> assert not is_error(check(1, int)) - >>> assert is_error(check(1, list)) - >>> assert is_error(check(1.3, int)) - >>> assert is_error(check(1.3, Union[str, int])) - """ - if isinstance(value, MagicMock): - # testing - any magic value is ignored - return None - if not isinstance(value, type) and dataclasses.is_dataclass(ty): - # dataclass - return check_dataclass(value, ty) - if is_typeddict(ty): - # should use `typing.is_typeddict` in future - return check_typeddict(value, ty) - to = get_origin(ty) - if to is not None: - # generics - err = check(value, to) - if is_error(err): - return err - - if to is list or to is set or to is frozenset: - err = check_mono_container(value, ty) - elif to is dict: - err = check_dict(value, ty) # type: ignore - elif to is tuple: - err = check_tuple(value, ty) - elif to is Literal: - err = check_literal(value, ty) - elif to is Union or is_pep604_union(to): - err = check_union(value, ty) - elif to is type: - err = check_class(value, ty) - return err - if isinstance(ty, type): - # concrete type - if is_pep604_union(ty): - pass # pragma: no cover - elif issubclass(ty, bool): - if not isinstance(value, ty): - return AutonomyTypeError(ty=ty, value=value) - elif issubclass(ty, int): # For boolean - return check_int(value, ty) - elif ty is typing.Any: - # `isinstance(value, typing.Any) fails on python 3.11` - # https://stackoverflow.com/questions/68031358/typeerror-typing-any-cannot-be-used-with-isinstance - pass - elif not isinstance(value, ty): - return AutonomyTypeError(ty=ty, value=value) - return None - - -def check_class(value: Any, ty: Type[Any]) -> Result: - """Check class type.""" - if not issubclass(value, get_args(ty)): - return AutonomyTypeError(ty=ty, value=value) - return None - - -def check_int(value: Any, ty: Type[Any]) -> Result: - """Check int type.""" - if isinstance(value, bool) or not isinstance(value, ty): - return AutonomyTypeError(ty=ty, value=value) - return None - - -def check_literal(value: Any, ty: Type[Any]) -> Result: - """Check literal type.""" - if all(value != t for t in get_args(ty)): - return AutonomyTypeError(ty=ty, value=value) - return None - - -def check_tuple(value: Any, ty: Type[Tuple[Any, ...]]) -> Result: - """Check tuple type.""" - types_ = get_args(ty) - if len(types_) == 2 and types_[1] == ...: - # arbitrary length tuple (e.g. Tuple[int, ...]) - for v in value: - err = check(v, types_[0]) - if is_error(err): - return err - return None - - if len(value) != len(types_): - return AutonomyTypeError(ty=ty, value=value) - for v, t in zip(value, types_): - err = check(v, t) - if is_error(err): - return err - return None - - -def check_union(value: Any, ty: Type[Any]) -> Result: - """Check union type.""" - if any(not is_error(check(value, t)) for t in get_args(ty)): - return None - return AutonomyTypeError(ty=ty, value=value) - - -def check_mono_container( - value: Any, ty: Union[Type[List[Any]], Type[Set[Any]], Type[FrozenSet[Any]]] -) -> Result: - """Check mono container type.""" - ty_item = get_args(ty)[0] - for v in value: - err = check(v, ty_item) - if is_error(err): - return err - return None - - -def check_dict(value: Dict[Any, Any], ty: Type[Dict[Any, Any]]) -> Result: - """Check dict type.""" - args = get_args(ty) - ty_key = args[0] - ty_item = args[1] - for k, v in value.items(): - err = check(k, ty_key) - if is_error(err): - return err - err = check(v, ty_item) - if err is not None: - err.path.append(k) - return err - return None - - -def check_dataclass(value: Any, ty: Type[Any]) -> Result: - """Check dataclass type.""" - if not dataclasses.is_dataclass(value): - return AutonomyTypeError(ty, value) - for k, ty_ in typing.get_type_hints(ty).items(): - v = getattr(value, k) - err = check(v, ty_) - if err is not None: - err.path.append(k) - return err - return None - - -def check_typeddict(value: Any, ty: Type[Any]) -> Result: - """Check typeddict type.""" - if not isinstance(value, dict): - return AutonomyTypeError(ty, value) # pragma: no cover - is_total: bool = ty.__total__ # type: ignore - for k, ty_ in typing.get_type_hints(ty).items(): - if k not in value: - if is_total: - return AutonomyTypeError(ty_, value, [k]) - continue - v = value[k] - err = check(v, ty_) - if err is not None: - err.path.append(k) - return err - return None - - -# TODO: incorporate -def is_typevar(ty: Type[Any]) -> TypeGuard[TypeVar]: - """Check typevar.""" - return isinstance(ty, TypeVar) # pragma: no cover - - -def is_error(ret: Result) -> TypeGuard[AutonomyTypeError]: - """Check error.""" - return ret is not None - - -def is_typeddict(ty: Type[Any]) -> TypeGuard[Type[TypedDict]]: # type: ignore - """Check typeddict.""" - # TODO: Should use `typing.is_typeddict` in future - # or, use publich API - T = "_TypedDictMeta" - for mod in [typing, typing_extensions]: - if hasattr(mod, T) and isinstance(ty, getattr(mod, T)): - return True - return False - - -def check_type(name: str, value: Any, type_hint: Any) -> None: - """Check value against type hint recursively""" - err = check(value, type_hint) - if err is not None: - err.path.append(name) - raise err - - -def is_primitive_or_none(obj: Any) -> bool: - """Checks if the given object is a primitive type or `None`.""" - primitives = (bool, int, float, str) - return isinstance(obj, primitives) or obj is None - - -def is_json_serializable(obj: Any) -> bool: - """Checks if the given object is json serializable.""" - if isinstance(obj, (tuple, list)): - return all(is_json_serializable(x) for x in obj) - if isinstance(obj, dict): - return all( - is_primitive_or_none(k) and is_json_serializable(v) for k, v in obj.items() - ) - - return is_primitive_or_none(obj) - - -def filter_negative(mapping: Dict[str, int]) -> Iterator[str]: - """Return the keys of a dictionary for which the values are negative integers.""" - return (key for key, number in mapping.items() if number < 0) - - -def consensus_threshold(nb: int) -> int: - """ - Get consensus threshold. - - :param nb: the number of participants - :return: the consensus threshold - """ - return ceil((2 * nb + 1) / 3) - - -KeyType = TypeVar("KeyType") -ValueType = TypeVar("ValueType") - - -def inverse(dict_: Dict[KeyType, ValueType]) -> Dict[ValueType, List[KeyType]]: - """Get the inverse of a dictionary.""" - inverse_: Dict[ValueType, List[KeyType]] = {val: [] for val in dict_.values()} - for key, value in dict_.items(): - inverse_[value].append(key) - return inverse_ From 41001715ba1513645bfe08115c62ac60c9717143 Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Tue, 22 Oct 2024 19:16:55 +0530 Subject: [PATCH 12/41] chore: Remove unused ABCI skills --- .../tests/data/dummy_abci/__init__.py | 28 - .../tests/data/dummy_abci/behaviours.py | 158 -- .../tests/data/dummy_abci/dialogues.py | 81 - .../tests/data/dummy_abci/handlers.py | 47 - .../tests/data/dummy_abci/models.py | 44 - .../tests/data/dummy_abci/payloads.py | 53 - .../tests/data/dummy_abci/rounds.py | 152 -- .../tests/data/dummy_abci/skill.yaml | 148 -- .../tests/test_io/__init__.py | 20 - .../tests/test_io/test_ipfs.py | 110 -- .../tests/test_io/test_load.py | 114 -- .../tests/test_io/test_store.py | 104 -- .../tests/test_tools/__init__.py | 20 - .../tests/test_tools/base.py | 86 - .../tests/test_tools/test_base.py | 189 --- .../tests/test_tools/test_common.py | 183 --- .../tests/test_tools/test_integration.py | 143 -- .../tests/test_tools/test_rounds.py | 659 -------- .../ipfs_package_downloader/__init__.py | 25 - .../ipfs_package_downloader/behaviours.py | 215 --- .../ipfs_package_downloader/dialogues.py | 61 - .../ipfs_package_downloader/handlers.py | 89 -- .../skills/ipfs_package_downloader/models.py | 68 - .../skills/ipfs_package_downloader/skill.yaml | 57 - .../ipfs_package_downloader/utils/__init__.py | 19 - .../ipfs_package_downloader/utils/ipfs.py | 94 -- .../ipfs_package_downloader/utils/task.py | 36 - .../market_data_fetcher_abci/__init__.py | 25 - .../market_data_fetcher_abci/behaviours.py | 449 ------ .../market_data_fetcher_abci/dialogues.py | 101 -- .../fsm_specification.yaml | 26 - .../market_data_fetcher_abci/handlers.py | 66 - .../skills/market_data_fetcher_abci/models.py | 187 --- .../market_data_fetcher_abci/payloads.py | 39 - .../skills/market_data_fetcher_abci/rounds.py | 175 --- .../market_data_fetcher_abci/skill.yaml | 165 -- .../skills/portfolio_tracker_abci/README.md | 6 - .../skills/portfolio_tracker_abci/__init__.py | 25 - .../portfolio_tracker_abci/behaviours.py | 545 ------- .../portfolio_tracker_abci/dialogues.py | 101 -- .../fsm_specification.yaml | 23 - .../skills/portfolio_tracker_abci/handlers.py | 66 - .../skills/portfolio_tracker_abci/models.py | 100 -- .../skills/portfolio_tracker_abci/payloads.py | 33 - .../skills/portfolio_tracker_abci/rounds.py | 167 -- .../skills/portfolio_tracker_abci/skill.yaml | 158 -- .../valory/skills/registration_abci/README.md | 25 - .../skills/registration_abci/__init__.py | 25 - .../skills/registration_abci/behaviours.py | 492 ------ .../skills/registration_abci/dialogues.py | 90 -- .../registration_abci/fsm_specification.yaml | 18 - .../skills/registration_abci/handlers.py | 51 - .../valory/skills/registration_abci/models.py | 41 - .../skills/registration_abci/payloads.py | 31 - .../valory/skills/registration_abci/rounds.py | 178 --- .../skills/registration_abci/skill.yaml | 151 -- .../registration_abci/tests/__init__.py | 20 - .../tests/test_behaviours.py | 644 -------- .../registration_abci/tests/test_dialogues.py | 28 - .../registration_abci/tests/test_handlers.py | 28 - .../registration_abci/tests/test_models.py | 35 - .../registration_abci/tests/test_payloads.py | 46 - .../registration_abci/tests/test_rounds.py | 371 ----- .../valory/skills/reset_pause_abci/README.md | 23 - .../skills/reset_pause_abci/__init__.py | 25 - .../skills/reset_pause_abci/behaviours.py | 99 -- .../skills/reset_pause_abci/dialogues.py | 91 -- .../reset_pause_abci/fsm_specification.yaml | 19 - .../skills/reset_pause_abci/handlers.py | 51 - .../valory/skills/reset_pause_abci/models.py | 55 - .../skills/reset_pause_abci/payloads.py | 31 - .../valory/skills/reset_pause_abci/rounds.py | 115 -- .../valory/skills/reset_pause_abci/skill.yaml | 141 -- .../skills/reset_pause_abci/tests/__init__.py | 20 - .../reset_pause_abci/tests/test_behaviours.py | 154 -- .../reset_pause_abci/tests/test_dialogues.py | 28 - .../reset_pause_abci/tests/test_handlers.py | 28 - .../reset_pause_abci/tests/test_payloads.py | 34 - .../reset_pause_abci/tests/test_rounds.py | 106 -- .../skills/strategy_evaluator_abci/README.md | 6 - .../strategy_evaluator_abci/__init__.py | 25 - .../behaviours/__init__.py | 20 - .../behaviours/backtesting.py | 132 -- .../behaviours/base.py | 293 ---- .../behaviours/prepare_swap_tx.py | 412 ----- .../behaviours/proxy_swap_queue.py | 147 -- .../behaviours/round_behaviour.py | 61 - .../behaviours/strategy_exec.py | 351 ----- .../behaviours/swap_queue.py | 91 -- .../strategy_evaluator_abci/dialogues.py | 100 -- .../fsm_specification.yaml | 85 - .../strategy_evaluator_abci/handlers.py | 67 - .../skills/strategy_evaluator_abci/models.py | 148 -- .../strategy_evaluator_abci/payloads.py | 55 - .../skills/strategy_evaluator_abci/rounds.py | 208 --- .../skills/strategy_evaluator_abci/skill.yaml | 257 --- .../states/__init__.py | 20 - .../states/backtesting.py | 52 - .../strategy_evaluator_abci/states/base.py | 182 --- .../states/final_states.py | 55 - .../states/prepare_swap.py | 59 - .../states/proxy_swap_queue.py | 57 - .../states/strategy_exec.py | 41 - .../states/swap_queue.py | 59 - .../trader_decision_maker_abci/README.md | 5 - .../trader_decision_maker_abci/__init__.py | 25 - .../trader_decision_maker_abci/behaviours.py | 234 --- .../trader_decision_maker_abci/dialogues.py | 90 -- .../fsm_specification.yaml | 25 - .../trader_decision_maker_abci/handlers.py | 50 - .../trader_decision_maker_abci/models.py | 54 - .../trader_decision_maker_abci/payloads.py | 42 - .../trader_decision_maker_abci/policy.py | 129 -- .../trader_decision_maker_abci/rounds.py | 232 --- .../trader_decision_maker_abci/skill.yaml | 142 -- .../trader_decision_maker_abci/utils.py | 34 - .../transaction_settlement_abci/README.md | 53 - .../transaction_settlement_abci/__init__.py | 25 - .../transaction_settlement_abci/behaviours.py | 984 ------------ .../transaction_settlement_abci/dialogues.py | 91 -- .../fsm_specification.yaml | 88 -- .../transaction_settlement_abci/handlers.py | 51 - .../transaction_settlement_abci/models.py | 123 -- .../payload_tools.py | 183 --- .../transaction_settlement_abci/payloads.py | 82 - .../transaction_settlement_abci/rounds.py | 831 ---------- .../transaction_settlement_abci/skill.yaml | 174 --- .../test_tools/__init__.py | 20 - .../test_tools/integration.py | 338 ---- .../tests/__init__.py | 24 - .../tests/test_behaviours.py | 1392 ----------------- .../tests/test_dialogues.py | 28 - .../tests/test_handlers.py | 28 - .../tests/test_models.py | 95 -- .../tests/test_payload_tools.py | 101 -- .../tests/test_payloads.py | 126 -- .../tests/test_rounds.py | 1023 ------------ .../tests/test_tools/__init__.py | 20 - .../tests/test_tools/test_integration.py | 214 --- 139 files changed, 18893 deletions(-) delete mode 100644 packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/__init__.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/behaviours.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/dialogues.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/handlers.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/models.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/payloads.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/rounds.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/skill.yaml delete mode 100644 packages/valory/skills/abstract_round_abci/tests/test_io/__init__.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/test_io/test_ipfs.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/test_io/test_load.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/test_io/test_store.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/test_tools/__init__.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/test_tools/base.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/test_tools/test_base.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/test_tools/test_common.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/test_tools/test_integration.py delete mode 100644 packages/valory/skills/abstract_round_abci/tests/test_tools/test_rounds.py delete mode 100644 packages/valory/skills/ipfs_package_downloader/__init__.py delete mode 100644 packages/valory/skills/ipfs_package_downloader/behaviours.py delete mode 100644 packages/valory/skills/ipfs_package_downloader/dialogues.py delete mode 100644 packages/valory/skills/ipfs_package_downloader/handlers.py delete mode 100644 packages/valory/skills/ipfs_package_downloader/models.py delete mode 100644 packages/valory/skills/ipfs_package_downloader/skill.yaml delete mode 100644 packages/valory/skills/ipfs_package_downloader/utils/__init__.py delete mode 100644 packages/valory/skills/ipfs_package_downloader/utils/ipfs.py delete mode 100644 packages/valory/skills/ipfs_package_downloader/utils/task.py delete mode 100644 packages/valory/skills/market_data_fetcher_abci/__init__.py delete mode 100644 packages/valory/skills/market_data_fetcher_abci/behaviours.py delete mode 100644 packages/valory/skills/market_data_fetcher_abci/dialogues.py delete mode 100644 packages/valory/skills/market_data_fetcher_abci/fsm_specification.yaml delete mode 100644 packages/valory/skills/market_data_fetcher_abci/handlers.py delete mode 100644 packages/valory/skills/market_data_fetcher_abci/models.py delete mode 100644 packages/valory/skills/market_data_fetcher_abci/payloads.py delete mode 100644 packages/valory/skills/market_data_fetcher_abci/rounds.py delete mode 100644 packages/valory/skills/market_data_fetcher_abci/skill.yaml delete mode 100644 packages/valory/skills/portfolio_tracker_abci/README.md delete mode 100644 packages/valory/skills/portfolio_tracker_abci/__init__.py delete mode 100644 packages/valory/skills/portfolio_tracker_abci/behaviours.py delete mode 100644 packages/valory/skills/portfolio_tracker_abci/dialogues.py delete mode 100644 packages/valory/skills/portfolio_tracker_abci/fsm_specification.yaml delete mode 100644 packages/valory/skills/portfolio_tracker_abci/handlers.py delete mode 100644 packages/valory/skills/portfolio_tracker_abci/models.py delete mode 100644 packages/valory/skills/portfolio_tracker_abci/payloads.py delete mode 100644 packages/valory/skills/portfolio_tracker_abci/rounds.py delete mode 100644 packages/valory/skills/portfolio_tracker_abci/skill.yaml delete mode 100644 packages/valory/skills/registration_abci/README.md delete mode 100644 packages/valory/skills/registration_abci/__init__.py delete mode 100644 packages/valory/skills/registration_abci/behaviours.py delete mode 100644 packages/valory/skills/registration_abci/dialogues.py delete mode 100644 packages/valory/skills/registration_abci/fsm_specification.yaml delete mode 100644 packages/valory/skills/registration_abci/handlers.py delete mode 100644 packages/valory/skills/registration_abci/models.py delete mode 100644 packages/valory/skills/registration_abci/payloads.py delete mode 100644 packages/valory/skills/registration_abci/rounds.py delete mode 100644 packages/valory/skills/registration_abci/skill.yaml delete mode 100644 packages/valory/skills/registration_abci/tests/__init__.py delete mode 100644 packages/valory/skills/registration_abci/tests/test_behaviours.py delete mode 100644 packages/valory/skills/registration_abci/tests/test_dialogues.py delete mode 100644 packages/valory/skills/registration_abci/tests/test_handlers.py delete mode 100644 packages/valory/skills/registration_abci/tests/test_models.py delete mode 100644 packages/valory/skills/registration_abci/tests/test_payloads.py delete mode 100644 packages/valory/skills/registration_abci/tests/test_rounds.py delete mode 100644 packages/valory/skills/reset_pause_abci/README.md delete mode 100644 packages/valory/skills/reset_pause_abci/__init__.py delete mode 100644 packages/valory/skills/reset_pause_abci/behaviours.py delete mode 100644 packages/valory/skills/reset_pause_abci/dialogues.py delete mode 100644 packages/valory/skills/reset_pause_abci/fsm_specification.yaml delete mode 100644 packages/valory/skills/reset_pause_abci/handlers.py delete mode 100644 packages/valory/skills/reset_pause_abci/models.py delete mode 100644 packages/valory/skills/reset_pause_abci/payloads.py delete mode 100644 packages/valory/skills/reset_pause_abci/rounds.py delete mode 100644 packages/valory/skills/reset_pause_abci/skill.yaml delete mode 100644 packages/valory/skills/reset_pause_abci/tests/__init__.py delete mode 100644 packages/valory/skills/reset_pause_abci/tests/test_behaviours.py delete mode 100644 packages/valory/skills/reset_pause_abci/tests/test_dialogues.py delete mode 100644 packages/valory/skills/reset_pause_abci/tests/test_handlers.py delete mode 100644 packages/valory/skills/reset_pause_abci/tests/test_payloads.py delete mode 100644 packages/valory/skills/reset_pause_abci/tests/test_rounds.py delete mode 100644 packages/valory/skills/strategy_evaluator_abci/README.md delete mode 100644 packages/valory/skills/strategy_evaluator_abci/__init__.py delete mode 100644 packages/valory/skills/strategy_evaluator_abci/behaviours/__init__.py delete mode 100644 packages/valory/skills/strategy_evaluator_abci/behaviours/backtesting.py delete mode 100644 packages/valory/skills/strategy_evaluator_abci/behaviours/base.py delete mode 100644 packages/valory/skills/strategy_evaluator_abci/behaviours/prepare_swap_tx.py delete mode 100644 packages/valory/skills/strategy_evaluator_abci/behaviours/proxy_swap_queue.py delete mode 100644 packages/valory/skills/strategy_evaluator_abci/behaviours/round_behaviour.py delete mode 100644 packages/valory/skills/strategy_evaluator_abci/behaviours/strategy_exec.py delete mode 100644 packages/valory/skills/strategy_evaluator_abci/behaviours/swap_queue.py delete mode 100644 packages/valory/skills/strategy_evaluator_abci/dialogues.py delete mode 100644 packages/valory/skills/strategy_evaluator_abci/fsm_specification.yaml delete mode 100644 packages/valory/skills/strategy_evaluator_abci/handlers.py delete mode 100644 packages/valory/skills/strategy_evaluator_abci/models.py delete mode 100644 packages/valory/skills/strategy_evaluator_abci/payloads.py delete mode 100644 packages/valory/skills/strategy_evaluator_abci/rounds.py delete mode 100644 packages/valory/skills/strategy_evaluator_abci/skill.yaml delete mode 100644 packages/valory/skills/strategy_evaluator_abci/states/__init__.py delete mode 100644 packages/valory/skills/strategy_evaluator_abci/states/backtesting.py delete mode 100644 packages/valory/skills/strategy_evaluator_abci/states/base.py delete mode 100644 packages/valory/skills/strategy_evaluator_abci/states/final_states.py delete mode 100644 packages/valory/skills/strategy_evaluator_abci/states/prepare_swap.py delete mode 100644 packages/valory/skills/strategy_evaluator_abci/states/proxy_swap_queue.py delete mode 100644 packages/valory/skills/strategy_evaluator_abci/states/strategy_exec.py delete mode 100644 packages/valory/skills/strategy_evaluator_abci/states/swap_queue.py delete mode 100644 packages/valory/skills/trader_decision_maker_abci/README.md delete mode 100644 packages/valory/skills/trader_decision_maker_abci/__init__.py delete mode 100644 packages/valory/skills/trader_decision_maker_abci/behaviours.py delete mode 100644 packages/valory/skills/trader_decision_maker_abci/dialogues.py delete mode 100644 packages/valory/skills/trader_decision_maker_abci/fsm_specification.yaml delete mode 100644 packages/valory/skills/trader_decision_maker_abci/handlers.py delete mode 100644 packages/valory/skills/trader_decision_maker_abci/models.py delete mode 100644 packages/valory/skills/trader_decision_maker_abci/payloads.py delete mode 100644 packages/valory/skills/trader_decision_maker_abci/policy.py delete mode 100644 packages/valory/skills/trader_decision_maker_abci/rounds.py delete mode 100644 packages/valory/skills/trader_decision_maker_abci/skill.yaml delete mode 100644 packages/valory/skills/trader_decision_maker_abci/utils.py delete mode 100644 packages/valory/skills/transaction_settlement_abci/README.md delete mode 100644 packages/valory/skills/transaction_settlement_abci/__init__.py delete mode 100644 packages/valory/skills/transaction_settlement_abci/behaviours.py delete mode 100644 packages/valory/skills/transaction_settlement_abci/dialogues.py delete mode 100644 packages/valory/skills/transaction_settlement_abci/fsm_specification.yaml delete mode 100644 packages/valory/skills/transaction_settlement_abci/handlers.py delete mode 100644 packages/valory/skills/transaction_settlement_abci/models.py delete mode 100644 packages/valory/skills/transaction_settlement_abci/payload_tools.py delete mode 100644 packages/valory/skills/transaction_settlement_abci/payloads.py delete mode 100644 packages/valory/skills/transaction_settlement_abci/rounds.py delete mode 100644 packages/valory/skills/transaction_settlement_abci/skill.yaml delete mode 100644 packages/valory/skills/transaction_settlement_abci/test_tools/__init__.py delete mode 100644 packages/valory/skills/transaction_settlement_abci/test_tools/integration.py delete mode 100644 packages/valory/skills/transaction_settlement_abci/tests/__init__.py delete mode 100644 packages/valory/skills/transaction_settlement_abci/tests/test_behaviours.py delete mode 100644 packages/valory/skills/transaction_settlement_abci/tests/test_dialogues.py delete mode 100644 packages/valory/skills/transaction_settlement_abci/tests/test_handlers.py delete mode 100644 packages/valory/skills/transaction_settlement_abci/tests/test_models.py delete mode 100644 packages/valory/skills/transaction_settlement_abci/tests/test_payload_tools.py delete mode 100644 packages/valory/skills/transaction_settlement_abci/tests/test_payloads.py delete mode 100644 packages/valory/skills/transaction_settlement_abci/tests/test_rounds.py delete mode 100644 packages/valory/skills/transaction_settlement_abci/tests/test_tools/__init__.py delete mode 100644 packages/valory/skills/transaction_settlement_abci/tests/test_tools/test_integration.py diff --git a/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/__init__.py b/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/__init__.py deleted file mode 100644 index c378e8c..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the implementation of the default skill.""" - -from pathlib import Path - -from aea.configurations.base import PublicId - - -PUBLIC_ID = PublicId.from_str("dummy/dummy_abci:0.1.0") -PATH_TO_SKILL = Path(__file__).parent diff --git a/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/behaviours.py b/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/behaviours.py deleted file mode 100644 index ee99805..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/behaviours.py +++ /dev/null @@ -1,158 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2022-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains round behaviours of DummyAbciApp.""" - -from abc import ABC -from collections import deque -from typing import Deque, Generator, Set, Type, cast - -from packages.valory.skills.abstract_round_abci.base import AbstractRound -from packages.valory.skills.abstract_round_abci.behaviours import ( - AbstractRoundBehaviour, - BaseBehaviour, -) -from packages.valory.skills.abstract_round_abci.common import ( - RandomnessBehaviour, - SelectKeeperBehaviour, -) -from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci.models import ( - Params, -) -from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci.payloads import ( - DummyFinalPayload, - DummyKeeperSelectionPayload, - DummyRandomnessPayload, - DummyStartingPayload, -) -from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci.rounds import ( - DummyAbciApp, - DummyFinalRound, - DummyKeeperSelectionRound, - DummyRandomnessRound, - DummyStartingRound, - SynchronizedData, -) - - -class DummyBaseBehaviour(BaseBehaviour, ABC): - """Base behaviour for the common apps' skill.""" - - @property - def synchronized_data(self) -> SynchronizedData: - """Return the synchronized data.""" - return cast(SynchronizedData, super().synchronized_data) - - @property - def params(self) -> Params: - """Return the params.""" - return cast(Params, super().params) - - -class DummyStartingBehaviour(DummyBaseBehaviour): - """DummyStartingBehaviour""" - - behaviour_id: str = "dummy_starting" - matching_round: Type[AbstractRound] = DummyStartingRound - - def async_act(self) -> Generator: - """Do the act, supporting asynchronous execution.""" - - with self.context.benchmark_tool.measure(self.behaviour_id).local(): - content = "dummy" - sender = self.context.agent_address - payload = DummyStartingPayload(sender=sender, content=content) - - with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): - yield from self.send_a2a_transaction(payload) - yield from self.wait_until_round_end() - - self.set_done() - - -class DummyRandomnessBehaviour(RandomnessBehaviour): - """DummyRandomnessBehaviour""" - - behaviour_id: str = "dummy_randomness" - matching_round: Type[AbstractRound] = DummyRandomnessRound - payload_class = DummyRandomnessPayload - - -class DummyKeeperSelectionBehaviour(SelectKeeperBehaviour): - """DummyKeeperSelectionBehaviour""" - - behaviour_id: str = "dummy_keeper_selection" - matching_round: Type[AbstractRound] = DummyKeeperSelectionRound - payload_class = DummyKeeperSelectionPayload - - @staticmethod - def serialized_keepers(keepers: Deque[str], keeper_retries: int = 1) -> str: - """Get the keepers serialized.""" - if not keepers: - return "" - return keeper_retries.to_bytes(32, "big").hex() + "".join(keepers) - - def async_act(self) -> Generator: - """Do the act, supporting asynchronous execution.""" - - with self.context.benchmark_tool.measure(self.behaviour_id).local(): - keepers = deque((self._select_keeper(),)) - payload = self.payload_class( - self.context.agent_address, self.serialized_keepers(keepers) - ) - - with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): - yield from self.send_a2a_transaction(payload) - yield from self.wait_until_round_end() - - self.set_done() - - -class DummyFinalBehaviour(DummyBaseBehaviour): - """DummyFinalBehaviour""" - - behaviour_id: str = "dummy_final" - matching_round: Type[AbstractRound] = DummyFinalRound - - def async_act(self) -> Generator: - """Do the act, supporting asynchronous execution.""" - - with self.context.benchmark_tool.measure(self.behaviour_id).local(): - content = True - sender = self.context.agent_address - payload = DummyFinalPayload(sender=sender, content=content) - - with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): - yield from self.send_a2a_transaction(payload) - yield from self.wait_until_round_end() - - self.set_done() - - -class DummyRoundBehaviour(AbstractRoundBehaviour): - """DummyRoundBehaviour""" - - initial_behaviour_cls = DummyStartingBehaviour - abci_app_cls = DummyAbciApp - behaviours: Set[Type[BaseBehaviour]] = { - DummyFinalBehaviour, # type: ignore - DummyKeeperSelectionBehaviour, # type: ignore - DummyRandomnessBehaviour, # type: ignore - DummyStartingBehaviour, # type: ignore - } diff --git a/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/dialogues.py b/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/dialogues.py deleted file mode 100644 index 24aa94d..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/dialogues.py +++ /dev/null @@ -1,81 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the dialogues of the DummyAbciApp.""" - -from packages.valory.skills.abstract_round_abci.dialogues import ( - AbciDialogue as BaseAbciDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - AbciDialogues as BaseAbciDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - ContractApiDialogue as BaseContractApiDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - ContractApiDialogues as BaseContractApiDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - HttpDialogue as BaseHttpDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - HttpDialogues as BaseHttpDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - LedgerApiDialogue as BaseLedgerApiDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - LedgerApiDialogues as BaseLedgerApiDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - SigningDialogue as BaseSigningDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - SigningDialogues as BaseSigningDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - TendermintDialogue as BaseTendermintDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - TendermintDialogues as BaseTendermintDialogues, -) - - -AbciDialogue = BaseAbciDialogue -AbciDialogues = BaseAbciDialogues - - -HttpDialogue = BaseHttpDialogue -HttpDialogues = BaseHttpDialogues - - -SigningDialogue = BaseSigningDialogue -SigningDialogues = BaseSigningDialogues - - -LedgerApiDialogue = BaseLedgerApiDialogue -LedgerApiDialogues = BaseLedgerApiDialogues - - -ContractApiDialogue = BaseContractApiDialogue -ContractApiDialogues = BaseContractApiDialogues - - -TendermintDialogue = BaseTendermintDialogue -TendermintDialogues = BaseTendermintDialogues diff --git a/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/handlers.py b/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/handlers.py deleted file mode 100644 index fc0edb2..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/handlers.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the handlers for the skill of DummyAbciApp.""" - -from packages.valory.skills.abstract_round_abci.handlers import ( - ABCIRoundHandler as BaseABCIRoundHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - ContractApiHandler as BaseContractApiHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - HttpHandler as BaseHttpHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - LedgerApiHandler as BaseLedgerApiHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - SigningHandler as BaseSigningHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - TendermintHandler as BaseTendermintHandler, -) - - -ABCIRoundHandler = BaseABCIRoundHandler -HttpHandler = BaseHttpHandler -SigningHandler = BaseSigningHandler -LedgerApiHandler = BaseLedgerApiHandler -ContractApiHandler = BaseContractApiHandler -TendermintHandler = BaseTendermintHandler diff --git a/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/models.py b/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/models.py deleted file mode 100644 index 5fba375..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/models.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2022-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the shared state for the abci skill of DummyAbciApp.""" - -from packages.valory.skills.abstract_round_abci.models import ApiSpecs, BaseParams -from packages.valory.skills.abstract_round_abci.models import ( - BenchmarkTool as BaseBenchmarkTool, -) -from packages.valory.skills.abstract_round_abci.models import Requests as BaseRequests -from packages.valory.skills.abstract_round_abci.models import ( - SharedState as BaseSharedState, -) -from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci.rounds import ( - DummyAbciApp, -) - - -class SharedState(BaseSharedState): - """Keep the current shared state of the skill.""" - - abci_app_cls = DummyAbciApp - - -Params = BaseParams -Requests = BaseRequests -BenchmarkTool = BaseBenchmarkTool -RandomnessApi = ApiSpecs diff --git a/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/payloads.py b/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/payloads.py deleted file mode 100644 index bc4308b..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/payloads.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2022-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the transaction payloads of the DummyAbciApp.""" - -from dataclasses import dataclass - -from packages.valory.skills.abstract_round_abci.base import BaseTxPayload - - -@dataclass(frozen=True) -class DummyStartingPayload(BaseTxPayload): - """Represent a transaction payload for the DummyStartingRound.""" - - content: str - - -@dataclass(frozen=True) -class DummyRandomnessPayload(BaseTxPayload): - """Represent a transaction payload for the DummyRandomnessRound.""" - - round_id: int - randomness: str - - -@dataclass(frozen=True) -class DummyKeeperSelectionPayload(BaseTxPayload): - """Represent a transaction payload for the DummyKeeperSelectionRound.""" - - keepers: str - - -@dataclass(frozen=True) -class DummyFinalPayload(BaseTxPayload): - """Represent a transaction payload for the DummyFinalRound.""" - - content: bool diff --git a/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/rounds.py b/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/rounds.py deleted file mode 100644 index b3ee3bc..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/rounds.py +++ /dev/null @@ -1,152 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2022-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains the rounds of DummyAbciApp.""" - -from abc import ABC -from enum import Enum -from typing import FrozenSet, Optional, Set, Tuple, cast - -from packages.valory.skills.abstract_round_abci.base import ( - AbciApp, - AbciAppTransitionFunction, - AbstractRound, - AppState, - BaseSynchronizedData, - CollectSameUntilAllRound, - CollectSameUntilThresholdRound, - EventToTimeout, - OnlyKeeperSendsRound, -) -from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci.payloads import ( - DummyFinalPayload, - DummyKeeperSelectionPayload, - DummyRandomnessPayload, - DummyStartingPayload, -) - - -class Event(Enum): - """DummyAbciApp Events""" - - ROUND_TIMEOUT = "round_timeout" - NO_MAJORITY = "no_majority" - DONE = "done" - - -class SynchronizedData(BaseSynchronizedData): - """ - Class to represent the synchronized data. - - This data is replicated by the tendermint application. - """ - - -class DummyMixinRound(AbstractRound, ABC): - """DummyMixinRound""" - - done_event = Event.DONE - no_majority_event = Event.NO_MAJORITY - - @property - def synchronized_data(self) -> SynchronizedData: - """Return the synchronized data.""" - return cast(SynchronizedData, self._synchronized_data) - - -class DummyStartingRound(CollectSameUntilAllRound, DummyMixinRound): - """DummyStartingRound""" - - round_id: str = "dummy_starting" - payload_class = DummyStartingPayload - payload_attribute: str = "dummy_starting" - synchronized_data_class = SynchronizedData - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: - """Process the end of the block.""" - - if self.collection_threshold_reached: - synchronized_data = self.synchronized_data.update( - participants=tuple(sorted(self.collection)), - synchronized_data_class=SynchronizedData, - ) - return synchronized_data, Event.DONE - return None - - -class DummyRandomnessRound(CollectSameUntilThresholdRound, DummyMixinRound): - """DummyRandomnessRound""" - - round_id: str = "dummy_randomness" - payload_class = DummyRandomnessPayload - payload_attribute: str = "dummy_randomness" - collection_key = "participant_to_randomness" - selection_key = "most_voted_randomness" - synchronized_data_class = SynchronizedData - - -class DummyKeeperSelectionRound(CollectSameUntilThresholdRound, DummyMixinRound): - """DummyKeeperSelectionRound""" - - round_id: str = "dummy_keeper_selection" - payload_class = DummyKeeperSelectionPayload - payload_attribute: str = "dummy_keeper_selection" - collection_key = "participant_to_keeper" - selection_key = "most_voted_keeper" - synchronized_data_class = SynchronizedData - - -class DummyFinalRound(OnlyKeeperSendsRound, DummyMixinRound): - """DummyFinalRound""" - - round_id: str = "dummy_final" - payload_class = DummyFinalPayload - payload_attribute: str = "dummy_final" - synchronized_data_class = SynchronizedData - - -class DummyAbciApp(AbciApp[Event]): - """DummyAbciApp""" - - initial_round_cls: AppState = DummyStartingRound - transition_function: AbciAppTransitionFunction = { - DummyStartingRound: { - Event.DONE: DummyRandomnessRound, - Event.ROUND_TIMEOUT: DummyStartingRound, - Event.NO_MAJORITY: DummyStartingRound, - }, - DummyRandomnessRound: { - Event.DONE: DummyKeeperSelectionRound, - Event.ROUND_TIMEOUT: DummyRandomnessRound, - Event.NO_MAJORITY: DummyRandomnessRound, - }, - DummyKeeperSelectionRound: { - Event.DONE: DummyFinalRound, - Event.ROUND_TIMEOUT: DummyKeeperSelectionRound, - Event.NO_MAJORITY: DummyKeeperSelectionRound, - }, - DummyFinalRound: { - Event.DONE: DummyStartingRound, - Event.ROUND_TIMEOUT: DummyFinalRound, - Event.NO_MAJORITY: DummyFinalRound, - }, - } - final_states: Set[AppState] = set() - event_to_timeout: EventToTimeout = {} - cross_period_persisted_keys: FrozenSet[str] = frozenset() diff --git a/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/skill.yaml b/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/skill.yaml deleted file mode 100644 index 4ac35e9..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/data/dummy_abci/skill.yaml +++ /dev/null @@ -1,148 +0,0 @@ -name: dummy_abci -author: dummy -version: 0.1.0 -type: skill -description: The scaffold skill is a scaffold for your own skill implementation. -license: Apache-2.0 -aea_version: '>=1.0.0, <2.0.0' -fingerprint: - __init__.py: bafybeielxta5wbywv6cb2p65phk73zeodroif7imk33qc7sxgxrcelr62y - behaviours.py: bafybeicwwlex4z5ro6hrw5cdwxyp5742klcqsjwcn423wgg3jk6xclzwvi - dialogues.py: bafybeiaswubmqa7trhajbjn34okmpftk2sehsqrjg7znzrrd7j32xzx4vq - handlers.py: bafybeifik3ftljs63u7nm4gadxpqbcvqj53p7qftzzzfto3ioad57k3x3u - models.py: bafybeif4lp5i6an4z4kkquh3x3ttsvfctvsu5excmxahjywbbbo7g3js5y - payloads.py: bafybeidllmzsctg3m5jhawbt3kzk6ieodtvgwklrquqehtqtzzwhkxxg4a - rounds.py: bafybeiab5q6pzh544uuc672hksh4rv6a74dunt4ztdnqo4gw3hnzd452ti - tests/__init__.py: bafybeiaxqzwmh36bhquqztcyrkxjjkz5cctseqetglrwdezgnkjrtg2654 - tests/test_behaviours.py: bafybeich3uo67gdbxrxsivlrxfgpfuixupl6qtotxxp2qqpyqnck4i67eu - tests/test_dialogues.py: bafybeice2v4xnsjhhlnpbejnvpory5spmrewwcfsefzqzq3uhfyya5hypm - tests/test_handlers.py: bafybeidrfumnc743qh5s2ahf5rxu3rzrroygxwpbqa7jtqxg5kirjzedjm - tests/test_models.py: bafybeifuxjmpv3eet2zn7vc5btprakueqlk2ybc2fxgzbtiho5wdslkeb4 - tests/test_payloads.py: bafybeicvbisfw5prv6jw3is3vw6gehsplt3teyeo6dbeh37xazh4izeyhq - tests/test_rounds.py: bafybeihjepr2hubbgmb7jkeldbam3zmsgwn6nffif7zp4etqlv2bt5rsxy -fingerprint_ignore_patterns: [] -connections: [] -contracts: [] -protocols: [] -skills: -- valory/abstract_round_abci:0.1.0:bafybeifjnk2v3cw233ke5qhakurvdsex64c5runjctclrh7y64tyh7uqrq -behaviours: - main: - args: {} - class_name: DummyRoundBehaviour -handlers: - abci: - args: {} - class_name: ABCIRoundHandler - contract_api: - args: {} - class_name: ContractApiHandler - http: - args: {} - class_name: HttpHandler - ledger_api: - args: {} - class_name: LedgerApiHandler - signing: - args: {} - class_name: SigningHandler - tendermint: - args: {} - class_name: TendermintHandler -models: - abci_dialogues: - args: {} - class_name: AbciDialogues - benchmark_tool: - args: - log_dir: /logs - class_name: BenchmarkTool - contract_api_dialogues: - args: {} - class_name: ContractApiDialogues - http_dialogues: - args: {} - class_name: HttpDialogues - ledger_api_dialogues: - args: {} - class_name: LedgerApiDialogues - params: - args: - cleanup_history_depth: 1 - cleanup_history_depth_current: null - drand_public_key: 868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31 - genesis_config: - chain_id: chain-c4daS1 - consensus_params: - block: - max_bytes: '22020096' - max_gas: '-1' - time_iota_ms: '1000' - evidence: - max_age_duration: '172800000000000' - max_age_num_blocks: '100000' - max_bytes: '1048576' - validator: - pub_key_types: - - ed25519 - version: {} - genesis_time: '2022-05-20T16:00:21.735122717Z' - voting_power: '10' - keeper_timeout: 30.0 - max_healthcheck: 120 - reset_pause_duration: 10 - on_chain_service_id: null - reset_tendermint_after: 2 - retry_attempts: 400 - retry_timeout: 3 - round_timeout_seconds: 30.0 - service_id: dummy - service_registry_address: null - setup: - all_participants: - - '0x0000000000000000000000000000000000000000' - safe_contract_address: '0x0000000000000000000000000000000000000000' - consensus_threshold: null - sleep_time: 1 - tendermint_check_sleep_delay: 3 - tendermint_com_url: http://localhost:8080 - tendermint_max_retries: 5 - tendermint_url: http://localhost:26657 - request_timeout: 10.0 - request_retry_delay: 1.0 - tx_timeout: 10.0 - max_attempts: 10 - share_tm_config_on_startup: false - tendermint_p2p_url: localhost:26656 - use_termination: false - use_slashing: false - slash_cooldown_hours: 3 - slash_threshold_amount: 10_000_000_000_000_000 - light_slash_unit_amount: 5_000_000_000_000_000 - serious_slash_unit_amount: 8_000_000_000_000_000 - class_name: Params - randomness_api: - args: - api_id: cloudflare - headers: {} - method: GET - parameters: {} - response_key: null - response_type: dict - retries: 5 - url: https://drand.cloudflare.com/public/latest - class_name: RandomnessApi - requests: - args: {} - class_name: Requests - signing_dialogues: - args: {} - class_name: SigningDialogues - state: - args: {} - class_name: SharedState - tendermint_dialogues: - args: {} - class_name: TendermintDialogues -dependencies: {} -is_abstract: false diff --git a/packages/valory/skills/abstract_round_abci/tests/test_io/__init__.py b/packages/valory/skills/abstract_round_abci/tests/test_io/__init__.py deleted file mode 100644 index b640bcf..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/test_io/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Package for `io` testing.""" diff --git a/packages/valory/skills/abstract_round_abci/tests/test_io/test_ipfs.py b/packages/valory/skills/abstract_round_abci/tests/test_io/test_ipfs.py deleted file mode 100644 index ce81b61..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/test_io/test_ipfs.py +++ /dev/null @@ -1,110 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains tests for the `IPFS` interactions.""" - -# pylint: skip-file - -import os -from pathlib import PosixPath -from typing import Any, Dict, cast -from unittest import mock - -import pytest - -from packages.valory.skills.abstract_round_abci.io_.ipfs import ( - IPFSInteract, - IPFSInteractionError, -) -from packages.valory.skills.abstract_round_abci.io_.load import AbstractLoader -from packages.valory.skills.abstract_round_abci.io_.store import ( - AbstractStorer, - StoredJSONType, - SupportedFiletype, -) - - -use_ipfs_daemon = pytest.mark.usefixtures("ipfs_daemon") - - -class TestIPFSInteract: - """Test `IPFSInteract`.""" - - def setup(self) -> None: - """Setup test class.""" - self.ipfs_interact = IPFSInteract() - - @pytest.mark.parametrize("multiple", (True, False)) - def test_store_and_send_and_back( - self, - multiple: bool, - dummy_obj: StoredJSONType, - dummy_multiple_obj: Dict[str, StoredJSONType], - tmp_path: PosixPath, - ) -> None: - """Test store -> send -> download -> read of objects.""" - obj: StoredJSONType - if multiple: - obj = dummy_multiple_obj - filepath = "dummy_dir" - else: - obj = dummy_obj - filepath = "test_file.json" - - filepath = str(tmp_path / filepath) - serialized_objects = self.ipfs_interact.store( - filepath, obj, multiple, SupportedFiletype.JSON - ) - expected_objects = obj - actual_objects = cast( - Dict[str, Any], - self.ipfs_interact.load( - serialized_objects, - SupportedFiletype.JSON, - ), - ) - if multiple: - # here we manually remove the trailing the dir from the name. - # This is done by the IPFS connection under normal circumstances. - actual_objects = {os.path.basename(k): v for k, v in actual_objects.items()} - - assert actual_objects == expected_objects - - def test_store_fails(self, dummy_multiple_obj: Dict[str, StoredJSONType]) -> None: - """Tests when "store" fails.""" - dummy_filepath = "dummy_dir" - multiple = False - with mock.patch.object( - AbstractStorer, - "store", - side_effect=ValueError, - ), pytest.raises(IPFSInteractionError): - self.ipfs_interact.store( - dummy_filepath, dummy_multiple_obj, multiple, SupportedFiletype.JSON - ) - - def test_load_fails(self, dummy_multiple_obj: Dict[str, StoredJSONType]) -> None: - """Tests when "load" fails.""" - dummy_object = {"test": "test"} - with mock.patch.object( - AbstractLoader, - "load", - side_effect=ValueError, - ), pytest.raises(IPFSInteractionError): - self.ipfs_interact.load(dummy_object) diff --git a/packages/valory/skills/abstract_round_abci/tests/test_io/test_load.py b/packages/valory/skills/abstract_round_abci/tests/test_io/test_load.py deleted file mode 100644 index ae48cef..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/test_io/test_load.py +++ /dev/null @@ -1,114 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Tests for the loading functionality of abstract round abci.""" - -# pylint: skip-file - -import json -from typing import Optional, cast - -import pytest - -from packages.valory.skills.abstract_round_abci.io_.load import ( - CustomLoaderType, - JSONLoader, - Loader, - SupportedLoaderType, -) -from packages.valory.skills.abstract_round_abci.io_.store import SupportedFiletype - - -class TestLoader: - """Tests for the `Loader`.""" - - def setup(self) -> None: - """Setup the tests.""" - self.json_loader = Loader(SupportedFiletype.JSON, None) - - def __dummy_custom_loader(self) -> None: - """A dummy custom loading function to use for the tests.""" - - @staticmethod - @pytest.mark.parametrize( - "filetype, custom_loader, expected_loader", - ( - (None, None, None), - (SupportedFiletype.JSON, None, JSONLoader.load_single_object), - ( - SupportedFiletype.JSON, - __dummy_custom_loader, - JSONLoader.load_single_object, - ), - (None, __dummy_custom_loader, __dummy_custom_loader), - ), - ) - def test__get_loader_from_filetype( - filetype: Optional[SupportedFiletype], - custom_loader: CustomLoaderType, - expected_loader: Optional[SupportedLoaderType], - ) -> None: - """Test `_get_loader_from_filetype`.""" - if all( - test_arg is None for test_arg in (filetype, custom_loader, expected_loader) - ): - with pytest.raises( - ValueError, - match="Please provide either a supported filetype or a custom loader function.", - ): - Loader(filetype, custom_loader)._get_single_loader_from_filetype() - - else: - expected_loader = cast(SupportedLoaderType, expected_loader) - loader = Loader(filetype, custom_loader) - assert ( - loader._get_single_loader_from_filetype().__code__.co_code - == expected_loader.__code__.co_code - ) - - def test_load(self) -> None: - """Test `load`.""" - expected_object = dummy_object = {"test": "test"} - filename = "test" - serialized_object = json.dumps(dummy_object) - actual_object = self.json_loader.load({filename: serialized_object}) - assert expected_object == actual_object - - def test_no_object(self) -> None: - """Test `load` throws error when no object is provided.""" - with pytest.raises( - ValueError, - match='"serialized_objects" does not contain any objects', - ): - self.json_loader.load({}) - - def test_load_multiple_objects(self) -> None: - """Test `load` when multiple objects are to be deserialized.""" - dummy_object = {"test": "test"} - serialized_object = json.dumps(dummy_object) - serialized_objects = { - "obj1": serialized_object, - "obj2": serialized_object, - } - expected_objects = { - "obj1": dummy_object, - "obj2": dummy_object, - } - actual_objects = self.json_loader.load(serialized_objects) - assert expected_objects == actual_objects diff --git a/packages/valory/skills/abstract_round_abci/tests/test_io/test_store.py b/packages/valory/skills/abstract_round_abci/tests/test_io/test_store.py deleted file mode 100644 index eae3d53..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/test_io/test_store.py +++ /dev/null @@ -1,104 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Tests for the storing functionality of abstract round abci.""" - -# pylint: skip-file - -import json -from pathlib import Path, PosixPath -from typing import Optional, cast - -import pytest - -from packages.valory.skills.abstract_round_abci.io_.store import ( - CustomStorerType, - JSONStorer, - Storer, - SupportedFiletype, - SupportedStorerType, -) - - -class TestStorer: - """Tests for the `Storer`.""" - - def setup(self) -> None: - """Setup the tests.""" - self.path = "tmp" - self.json_storer = Storer(SupportedFiletype.JSON, None, self.path) - - def __dummy_custom_storer(self) -> None: - """A dummy custom storing function to use for the tests.""" - - @staticmethod - @pytest.mark.parametrize( - "filetype, custom_storer, expected_storer", - ( - (None, None, None), - (SupportedFiletype.JSON, None, JSONStorer.serialize_object), - ( - SupportedFiletype.JSON, - __dummy_custom_storer, - JSONStorer.serialize_object, - ), - (None, __dummy_custom_storer, __dummy_custom_storer), - ), - ) - def test__get_single_storer_from_filetype( - filetype: Optional[SupportedFiletype], - custom_storer: Optional[CustomStorerType], - expected_storer: Optional[SupportedStorerType], - tmp_path: PosixPath, - ) -> None: - """Test `_get_single_storer_from_filetype`.""" - if all( - test_arg is None for test_arg in (filetype, custom_storer, expected_storer) - ): - with pytest.raises( - ValueError, - match="Please provide either a supported filetype or a custom storing function.", - ): - Storer( - filetype, custom_storer, str(tmp_path) - )._get_single_storer_from_filetype() - - else: - expected_storer = cast(SupportedStorerType, expected_storer) - storer = Storer(filetype, custom_storer, str(tmp_path)) - assert ( - storer._get_single_storer_from_filetype().__code__.co_code - == expected_storer.__code__.co_code - ) - - def test_store(self) -> None: - """Test `store`.""" - dummy_object = {"test": "test"} - expected_object = {self.path: json.dumps(dummy_object, indent=4)} - actual_object = self.json_storer.store(dummy_object, False) - assert expected_object == actual_object - - def test_store_multiple(self) -> None: - """Test `store` when multiple files are present.""" - dummy_object = {"test": "test"} - dummy_filename = "test" - expected_path = Path(f"{self.path}/{dummy_filename}").__str__() - expected_object = {expected_path: json.dumps(dummy_object, indent=4)} - actual_object = self.json_storer.store({dummy_filename: dummy_object}, True) - assert expected_object == actual_object diff --git a/packages/valory/skills/abstract_round_abci/tests/test_tools/__init__.py b/packages/valory/skills/abstract_round_abci/tests/test_tools/__init__.py deleted file mode 100644 index 261d2d3..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/test_tools/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Package for `test_tools` testing.""" diff --git a/packages/valory/skills/abstract_round_abci/tests/test_tools/base.py b/packages/valory/skills/abstract_round_abci/tests/test_tools/base.py deleted file mode 100644 index 8595574..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/test_tools/base.py +++ /dev/null @@ -1,86 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Tests for abstract_round_abci/test_tools/common.py""" - -from pathlib import Path -from typing import Any, Dict, Type, cast - -from aea.helpers.base import cd -from aea.test_tools.utils import copy_class - -from packages.valory.skills.abstract_round_abci.base import BaseTxPayload, _MetaPayload -from packages.valory.skills.abstract_round_abci.test_tools.base import ( - FSMBehaviourBaseCase, -) -from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci import ( - PATH_TO_SKILL, -) - - -class FSMBehaviourTestToolSetup: - """BaseRandomnessBehaviourTestSetup""" - - test_cls: Type[FSMBehaviourBaseCase] - __test_cls: Type[FSMBehaviourBaseCase] - __old_value: Dict[str, Type[BaseTxPayload]] - - @classmethod - def setup_class(cls) -> None: - """Setup class""" - - if not hasattr(cls, "test_cls"): - raise AttributeError(f"{cls} must set `test_cls`") - - cls.__test_cls = cls.test_cls - cls.__old_value = _MetaPayload.registry.copy() - _MetaPayload.registry.clear() - - @classmethod - def teardown_class(cls) -> None: - """Teardown class""" - _MetaPayload.registry = cls.__old_value - - def setup(self) -> None: - """Setup test""" - test_cls = copy_class(self.__test_cls) - self.test_cls = cast(Type[FSMBehaviourBaseCase], test_cls) - - def teardown(self) -> None: - """Teardown test""" - self.test_cls.teardown_class() - - def set_path_to_skill(self, path_to_skill: Path = PATH_TO_SKILL) -> None: - """Set path_to_skill""" - self.test_cls.path_to_skill = path_to_skill - - def setup_test_cls(self, **kwargs: Any) -> FSMBehaviourBaseCase: - """Helper method to setup test to be tested""" - - # different test tools will require the setting of - # different class attributes (such as path_to_skill). - # One should write a test that sets these, - # and subsequently invoke this method to test the setup. - - with cd(self.test_cls.path_to_skill): - self.test_cls.setup_class(**kwargs) - - test_instance = self.test_cls() - test_instance.setup() - return test_instance diff --git a/packages/valory/skills/abstract_round_abci/tests/test_tools/test_base.py b/packages/valory/skills/abstract_round_abci/tests/test_tools/test_base.py deleted file mode 100644 index 294c846..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/test_tools/test_base.py +++ /dev/null @@ -1,189 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2022-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Tests for abstract_round_abci/test_tools/base.py""" - -from enum import Enum -from typing import Any, Dict, cast - -import pytest -from aea.mail.base import Envelope - -from packages.valory.connections.ledger.connection import ( - PUBLIC_ID as LEDGER_CONNECTION_PUBLIC_ID, -) -from packages.valory.protocols.contract_api.message import ContractApiMessage -from packages.valory.protocols.ledger_api.message import LedgerApiMessage -from packages.valory.skills.abstract_round_abci.base import AbciAppDB -from packages.valory.skills.abstract_round_abci.behaviours import BaseBehaviour -from packages.valory.skills.abstract_round_abci.test_tools.base import ( - DummyContext, - FSMBehaviourBaseCase, -) -from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci import PUBLIC_ID -from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci.behaviours import ( - DummyRoundBehaviour, -) -from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci.models import ( - SharedState, -) -from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci.rounds import ( - Event, - SynchronizedData, -) -from packages.valory.skills.abstract_round_abci.tests.test_tools.base import ( - FSMBehaviourTestToolSetup, -) - - -class TestFSMBehaviourBaseCaseSetup(FSMBehaviourTestToolSetup): - """test TestFSMBehaviourBaseCaseSetup setup""" - - test_cls = FSMBehaviourBaseCase - - @pytest.mark.parametrize("kwargs", [{}]) - def test_setup_fails_without_path(self, kwargs: Dict[str, Dict[str, Any]]) -> None: - """Test setup""" - with pytest.raises(ValueError): - self.test_cls.setup_class(**kwargs) - - @pytest.mark.parametrize("kwargs", [{}, {"param_overrides": {"new_p": None}}]) - def test_setup(self, kwargs: Dict[str, Dict[str, Any]]) -> None: - """Test setup""" - - self.set_path_to_skill() - test_instance = self.setup_test_cls(**kwargs) - assert test_instance - assert hasattr(test_instance.behaviour.context.params, "new_p") == bool(kwargs) - - @pytest.mark.parametrize("behaviour", DummyRoundBehaviour.behaviours) - def test_fast_forward_to_behaviour(self, behaviour: BaseBehaviour) -> None: - """Test fast_forward_to_behaviour""" - self.set_path_to_skill() - test_instance = self.setup_test_cls() - - skill = test_instance._skill # pylint: disable=protected-access - round_behaviour = skill.skill_context.behaviours.main - behaviour_id = behaviour.behaviour_id - synchronized_data = SynchronizedData( - AbciAppDB(setup_data=dict(participants=[tuple("abcd")])) - ) - - test_instance.fast_forward_to_behaviour( - behaviour=round_behaviour, - behaviour_id=behaviour_id, - synchronized_data=synchronized_data, - ) - - current_behaviour = test_instance.behaviour.current_behaviour - assert current_behaviour is not None - assert isinstance( - current_behaviour.synchronized_data, - SynchronizedData, - ) - assert current_behaviour.behaviour_id == behaviour.behaviour_id - assert ( # pylint: disable=protected-access - test_instance.skill.skill_context.state.round_sequence.abci_app._current_round_cls - == current_behaviour.matching_round - == behaviour.matching_round - ) - - @pytest.mark.parametrize("event", Event) - @pytest.mark.parametrize("set_none", [False, True]) - def test_end_round(self, event: Enum, set_none: bool) -> None: - """Test end_round""" - - self.set_path_to_skill() - test_instance = self.setup_test_cls() - current_behaviour = cast( - BaseBehaviour, test_instance.behaviour.current_behaviour - ) - abci_app = current_behaviour.context.state.round_sequence.abci_app - if set_none: - test_instance.behaviour.current_behaviour = None - assert abci_app.current_round_height == 0 - test_instance.end_round(event) - assert abci_app.current_round_height == 1 - int(set_none) - - def test_mock_ledger_api_request(self) -> None: - """Test mock_ledger_api_request""" - - self.set_path_to_skill() - test_instance = self.setup_test_cls() - - request_kwargs = dict(performative=LedgerApiMessage.Performative.GET_BALANCE) - response_kwargs = dict(performative=LedgerApiMessage.Performative.BALANCE) - with pytest.raises( - AssertionError, - match="Invalid number of messages in outbox. Expected 1. Found 0.", - ): - test_instance.mock_ledger_api_request(request_kwargs, response_kwargs) - - message = LedgerApiMessage(**request_kwargs, dialogue_reference=("a", "b")) # type: ignore - envelope = Envelope( - to=str(LEDGER_CONNECTION_PUBLIC_ID), - sender=str(PUBLIC_ID), - protocol_specification_id=LedgerApiMessage.protocol_specification_id, - message=message, - ) - multiplexer = test_instance._multiplexer # pylint: disable=protected-access - multiplexer.out_queue.put_nowait(envelope) - test_instance.mock_ledger_api_request(request_kwargs, response_kwargs) - - def test_mock_contract_api_request(self) -> None: - """Test mock_contract_api_request""" - - self.set_path_to_skill() - test_instance = self.setup_test_cls() - - contract_id = "dummy_contract" - request_kwargs = dict(performative=ContractApiMessage.Performative.GET_STATE) - response_kwargs = dict(performative=ContractApiMessage.Performative.STATE) - with pytest.raises( - AssertionError, - match="Invalid number of messages in outbox. Expected 1. Found 0.", - ): - test_instance.mock_contract_api_request( - contract_id, request_kwargs, response_kwargs - ) - - message = ContractApiMessage( - **request_kwargs, # type: ignore - dialogue_reference=("a", "b"), - ledger_id="ethereum", - contract_id=contract_id - ) - envelope = Envelope( - to=str(LEDGER_CONNECTION_PUBLIC_ID), - sender=str(PUBLIC_ID), - protocol_specification_id=ContractApiMessage.protocol_specification_id, - message=message, - ) - multiplexer = test_instance._multiplexer # pylint: disable=protected-access - multiplexer.out_queue.put_nowait(envelope) - test_instance.mock_contract_api_request( - contract_id, request_kwargs, response_kwargs - ) - - -def test_dummy_context_is_abstract_component() -> None: - """Test dummy context is abstract component""" - - shared_state = SharedState(name="dummy_shared_state", skill_context=DummyContext()) - assert shared_state.context.is_abstract_component diff --git a/packages/valory/skills/abstract_round_abci/tests/test_tools/test_common.py b/packages/valory/skills/abstract_round_abci/tests/test_tools/test_common.py deleted file mode 100644 index b5cb5d2..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/test_tools/test_common.py +++ /dev/null @@ -1,183 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2022-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Tests for abstract_round_abci/test_tools/common.py""" - -from typing import Type, Union, cast - -import pytest - -from packages.valory.skills.abstract_round_abci.test_tools.common import ( - BaseRandomnessBehaviourTest, - BaseSelectKeeperBehaviourTest, -) -from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci import ( - PATH_TO_SKILL, -) -from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci.behaviours import ( - DummyFinalBehaviour, - DummyKeeperSelectionBehaviour, - DummyRandomnessBehaviour, -) -from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci.rounds import ( - Event, -) -from packages.valory.skills.abstract_round_abci.tests.test_tools.base import ( - FSMBehaviourTestToolSetup, -) - - -class BaseCommonBaseCaseTestSetup(FSMBehaviourTestToolSetup): - """BaseRandomnessBehaviourTestSetup""" - - test_cls: Type[Union[BaseRandomnessBehaviourTest, BaseSelectKeeperBehaviourTest]] - - def set_done_event(self) -> None: - """Set done_event""" - self.test_cls.done_event = Event.DONE - - def set_next_behaviour_class(self, next_behaviour_class: Type) -> None: - """Set next_behaviour_class""" - self.test_cls.next_behaviour_class = next_behaviour_class - - -class TestBaseRandomnessBehaviourTestSetup(BaseCommonBaseCaseTestSetup): - """Test BaseRandomnessBehaviourTest setup.""" - - test_cls: Type[BaseRandomnessBehaviourTest] = BaseRandomnessBehaviourTest - - def set_randomness_behaviour_class(self) -> None: - """Set randomness_behaviour_class""" - self.test_cls.randomness_behaviour_class = DummyRandomnessBehaviour # type: ignore - - def test_setup_randomness_behaviour_class_not_set(self) -> None: - """Test setup randomness_behaviour_class not set.""" - - self.set_path_to_skill() - test_instance = cast(BaseRandomnessBehaviourTest, self.setup_test_cls()) - expected = f"'{self.test_cls.__name__}' object has no attribute 'randomness_behaviour_class'" - with pytest.raises(AttributeError, match=expected): - test_instance.test_randomness_behaviour() - - def test_setup_done_event_not_set(self) -> None: - """Test setup done_event = Event.DONE not set.""" - - self.set_path_to_skill() - self.set_randomness_behaviour_class() - - test_instance = cast(BaseRandomnessBehaviourTest, self.setup_test_cls()) - expected = f"'{self.test_cls.__name__}' object has no attribute 'done_event'" - with pytest.raises(AttributeError, match=expected): - test_instance.test_randomness_behaviour() - - def test_setup_next_behaviour_class_not_set(self) -> None: - """Test setup next_behaviour_class not set.""" - - self.set_path_to_skill() - self.set_randomness_behaviour_class() - self.set_done_event() - - test_instance = cast(BaseRandomnessBehaviourTest, self.setup_test_cls()) - expected = ( - f"'{self.test_cls.__name__}' object has no attribute 'next_behaviour_class'" - ) - with pytest.raises(AttributeError, match=expected): - test_instance.test_randomness_behaviour() - - def test_successful_setup_randomness_behaviour_test(self) -> None: - """Test successful setup of the test class inheriting from BaseRandomnessBehaviourTest.""" - - self.set_path_to_skill() - self.set_randomness_behaviour_class() - self.set_done_event() - self.set_next_behaviour_class(DummyKeeperSelectionBehaviour) - test_instance = cast(BaseRandomnessBehaviourTest, self.setup_test_cls()) - test_instance.test_randomness_behaviour() - - -class TestBaseRandomnessBehaviourTestRunning(BaseRandomnessBehaviourTest): - """Test TestBaseRandomnessBehaviourTestRunning running.""" - - path_to_skill = PATH_TO_SKILL - randomness_behaviour_class = DummyRandomnessBehaviour - next_behaviour_class = DummyKeeperSelectionBehaviour - done_event = Event.DONE - - -class TestBaseSelectKeeperBehaviourTestSetup(BaseCommonBaseCaseTestSetup): - """Test BaseRandomnessBehaviourTest setup.""" - - test_cls: Type[BaseSelectKeeperBehaviourTest] = BaseSelectKeeperBehaviourTest - - def set_select_keeper_behaviour_class(self) -> None: - """Set select_keeper_behaviour_class""" - self.test_cls.select_keeper_behaviour_class = DummyKeeperSelectionBehaviour # type: ignore - - def test_setup_select_keeper_behaviour_class_not_set(self) -> None: - """Test setup select_keeper_behaviour_class not set.""" - - self.set_path_to_skill() - test_instance = cast(BaseSelectKeeperBehaviourTest, self.setup_test_cls()) - expected = f"'{self.test_cls.__name__}' object has no attribute 'select_keeper_behaviour_class'" - with pytest.raises(AttributeError, match=expected): - test_instance.test_select_keeper_preexisting_keeper() - - def test_setup_done_event_not_set(self) -> None: - """Test setup done_event = Event.DONE not set.""" - - self.set_path_to_skill() - self.set_select_keeper_behaviour_class() - - test_instance = cast(BaseSelectKeeperBehaviourTest, self.setup_test_cls()) - expected = f"'{self.test_cls.__name__}' object has no attribute 'done_event'" - with pytest.raises(AttributeError, match=expected): - test_instance.test_select_keeper_preexisting_keeper() - - def test_setup_next_behaviour_class_not_set(self) -> None: - """Test setup next_behaviour_class not set.""" - - self.set_path_to_skill() - self.set_select_keeper_behaviour_class() - self.set_done_event() - - test_instance = cast(BaseSelectKeeperBehaviourTest, self.setup_test_cls()) - expected = ( - f"'{self.test_cls.__name__}' object has no attribute 'next_behaviour_class'" - ) - with pytest.raises(AttributeError, match=expected): - test_instance.test_select_keeper_preexisting_keeper() - - def test_successful_setup_select_keeper_behaviour_test(self) -> None: - """Test successful setup of the test class inheriting from BaseSelectKeeperBehaviourTest.""" - - self.set_path_to_skill() - self.set_select_keeper_behaviour_class() - self.set_done_event() - self.set_next_behaviour_class(DummyFinalBehaviour) - test_instance = cast(BaseSelectKeeperBehaviourTest, self.setup_test_cls()) - test_instance.test_select_keeper_preexisting_keeper() - - -class TestBaseSelectKeeperBehaviourTestRunning(BaseSelectKeeperBehaviourTest): - """Test BaseSelectKeeperBehaviourTest running.""" - - path_to_skill = PATH_TO_SKILL - select_keeper_behaviour_class = DummyKeeperSelectionBehaviour - next_behaviour_class = DummyFinalBehaviour - done_event = Event.DONE diff --git a/packages/valory/skills/abstract_round_abci/tests/test_tools/test_integration.py b/packages/valory/skills/abstract_round_abci/tests/test_tools/test_integration.py deleted file mode 100644 index e71ed92..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/test_tools/test_integration.py +++ /dev/null @@ -1,143 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2022-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Tests for abstract_round_abci/test_tools/integration.py""" - -from typing import cast - -import pytest - -from packages.open_aea.protocols.signing import SigningMessage -from packages.open_aea.protocols.signing.custom_types import SignedMessage -from packages.valory.connections.ledger.connection import ( - PUBLIC_ID as LEDGER_CONNECTION_PUBLIC_ID, -) -from packages.valory.connections.ledger.tests.conftest import make_ledger_api_connection -from packages.valory.protocols.ledger_api import LedgerApiMessage -from packages.valory.protocols.ledger_api.dialogues import LedgerApiDialogue -from packages.valory.skills.abstract_round_abci.base import AbciAppDB -from packages.valory.skills.abstract_round_abci.behaviours import BaseBehaviour -from packages.valory.skills.abstract_round_abci.models import Requests -from packages.valory.skills.abstract_round_abci.test_tools.integration import ( - IntegrationBaseCase, -) -from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci.behaviours import ( - DummyStartingBehaviour, -) -from packages.valory.skills.abstract_round_abci.tests.data.dummy_abci.rounds import ( - SynchronizedData, -) -from packages.valory.skills.abstract_round_abci.tests.test_tools.base import ( - FSMBehaviourTestToolSetup, -) - - -def simulate_ledger_get_balance_request(test_instance: IntegrationBaseCase) -> None: - """Simulate ledger GET_BALANCE request""" - - ledger_api_dialogues = test_instance.skill.skill_context.ledger_api_dialogues - ledger_api_msg, ledger_api_dialogue = ledger_api_dialogues.create( - counterparty=str(LEDGER_CONNECTION_PUBLIC_ID), - performative=LedgerApiMessage.Performative.GET_BALANCE, - ledger_id="ethereum", - address="0x" + "0" * 40, - ) - ledger_api_dialogue = cast(LedgerApiDialogue, ledger_api_dialogue) - current_behaviour = cast(BaseBehaviour, test_instance.behaviour.current_behaviour) - request_nonce = current_behaviour._get_request_nonce_from_dialogue( # pylint: disable=protected-access - ledger_api_dialogue - ) - cast(Requests, current_behaviour.context.requests).request_id_to_callback[ - request_nonce - ] = current_behaviour.get_callback_request() - current_behaviour.context.outbox.put_message(message=ledger_api_msg) - - -class TestIntegrationBaseCase(FSMBehaviourTestToolSetup): - """TestIntegrationBaseCase""" - - test_cls = IntegrationBaseCase - - def test_instantiation(self) -> None: - """Test instantiation""" - - self.set_path_to_skill() - self.test_cls.make_ledger_api_connection_callable = make_ledger_api_connection - test_instance = cast(IntegrationBaseCase, self.setup_test_cls()) - - assert test_instance - assert test_instance.get_message_from_outbox() is None - assert test_instance.get_message_from_decision_maker_inbox() is None - assert test_instance.process_n_messages(ncycles=0) is tuple() - - expected = "Invalid number of messages in outbox. Expected 1. Found 0." - with pytest.raises(AssertionError, match=expected): - assert test_instance.process_message_cycle() - with pytest.raises(AssertionError, match=expected): - assert test_instance.process_n_messages(ncycles=1) - - def test_process_messages_cycle(self) -> None: - """Test process_message_cycle""" - - self.set_path_to_skill() - self.test_cls.make_ledger_api_connection_callable = make_ledger_api_connection - test_instance = cast(IntegrationBaseCase, self.setup_test_cls()) - - simulate_ledger_get_balance_request(test_instance) - message = test_instance.process_message_cycle( - handler=None, - ) - assert message is None - - simulate_ledger_get_balance_request(test_instance) - # connection error - cannot dynamically mix in an autouse fixture - message = test_instance.process_message_cycle( - handler=test_instance.ledger_handler, - expected_content={"performative": LedgerApiMessage.Performative.ERROR}, - ) - assert message - - def test_process_n_messages(self) -> None: - """Test process_n_messages""" - - self.set_path_to_skill() - self.test_cls.make_ledger_api_connection_callable = make_ledger_api_connection - test_instance = cast(IntegrationBaseCase, self.setup_test_cls()) - - behaviour_id = DummyStartingBehaviour.auto_behaviour_id() - synchronized_data = SynchronizedData( - AbciAppDB(setup_data=dict(participants=[tuple("abcd")])) - ) - - handlers = [test_instance.signing_handler] - expected_content = [ - {"performative": SigningMessage.Performative.SIGNED_MESSAGE} - ] - expected_types = [{"signed_message": SignedMessage}] - - messages = test_instance.process_n_messages( - ncycles=1, - behaviour_id=behaviour_id, - synchronized_data=synchronized_data, - handlers=handlers, # type: ignore - expected_content=expected_content, # type: ignore - expected_types=expected_types, # type: ignore - fail_send_a2a=True, - ) - assert len(messages) == 1 diff --git a/packages/valory/skills/abstract_round_abci/tests/test_tools/test_rounds.py b/packages/valory/skills/abstract_round_abci/tests/test_tools/test_rounds.py deleted file mode 100644 index 9fbbde9..0000000 --- a/packages/valory/skills/abstract_round_abci/tests/test_tools/test_rounds.py +++ /dev/null @@ -1,659 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2022-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - - -"""Test the `rounds` test tool module of the skill.""" - -import re -from enum import Enum -from typing import Any, FrozenSet, Generator, List, Optional, Tuple, Type, cast -from unittest.mock import MagicMock - -import pytest -from hypothesis import given, settings -from hypothesis import strategies as st - -from packages.valory.skills.abstract_round_abci.base import ( - AbciAppDB, - BaseSynchronizedData, -) -from packages.valory.skills.abstract_round_abci.test_tools.rounds import ( - BaseCollectDifferentUntilAllRoundTest, - BaseCollectDifferentUntilThresholdRoundTest, - BaseCollectSameUntilAllRoundTest, - BaseCollectSameUntilThresholdRoundTest, - BaseOnlyKeeperSendsRoundTest, - BaseRoundTestClass, - BaseVotingRoundTest, - DummyCollectDifferentUntilAllRound, - DummyCollectDifferentUntilThresholdRound, - DummyCollectSameUntilAllRound, - DummyCollectSameUntilThresholdRound, - DummyEvent, - DummyOnlyKeeperSendsRound, - DummySynchronizedData, - DummyTxPayload, - DummyVotingRound, - MAX_PARTICIPANTS, - get_dummy_tx_payloads, - get_participants, -) -from packages.valory.skills.abstract_round_abci.tests.conftest import profile_name -from packages.valory.skills.abstract_round_abci.tests.test_common import last_iteration - - -settings.load_profile(profile_name) - -# this is how many times we need to iterate before reaching the last iteration for a base test. -BASE_TEST_GEN_ITERATIONS = 4 - - -def test_get_participants() -> None: - """Test `get_participants`.""" - participants = get_participants() - assert isinstance(participants, frozenset) - assert all(isinstance(p, str) for p in participants) - assert len(participants) == MAX_PARTICIPANTS - - -class DummyTxPayloadMatcher: - """A `DummyTxPayload` matcher for assertion comparisons.""" - - expected: DummyTxPayload - - def __init__(self, expected: DummyTxPayload) -> None: - """Initialize the matcher.""" - self.expected = expected - - def __repr__(self) -> str: - """Needs to be implemented for better assertion messages.""" - return ( - "DummyTxPayload(" - f"id={repr(self.expected.id_)}, " - f"round_count={repr(self.expected.round_count)}, " - f"sender={repr(self.expected.sender)}, " - f"value={repr(self.expected.value)}, " - f"vote={repr(self.expected.vote)}" - ")" - ) - - def __eq__(self, other: Any) -> bool: - """The method that will be used for the assertion comparisons.""" - return ( - self.expected.round_count == other.round_count - and self.expected.sender == other.sender - and self.expected.value == other.value - and self.expected.vote == other.vote - ) - - -@given( - st.frozensets(st.text(max_size=200), max_size=100), - st.text(max_size=500), - st.one_of(st.none(), st.booleans()), - st.booleans(), -) -def test_get_dummy_tx_payloads( - participants: FrozenSet[str], - value: str, - vote: Optional[bool], - is_value_none: bool, -) -> None: - """Test `get_dummy_tx_payloads`.""" - expected = [ - DummyTxPayloadMatcher( - DummyTxPayload( - sender=agent, - value=(value or agent) if not is_value_none else value, - vote=vote, - ) - ) - for agent in sorted(participants) - ] - - actual = get_dummy_tx_payloads(participants, value, vote, is_value_none) - - assert len(actual) == len(expected) == len(participants) - assert actual == expected - - -class TestDummyTxPayload: # pylint: disable=too-few-public-methods - """Test class for `DummyTxPayload`""" - - @staticmethod - @given(st.text(max_size=200), st.text(max_size=500), st.booleans()) - def test_properties( - sender: str, - value: str, - vote: bool, - ) -> None: - """Test all the properties.""" - dummy_tx_payload = DummyTxPayload(sender, value, vote) - assert dummy_tx_payload.value == value - assert dummy_tx_payload.vote == vote - assert dummy_tx_payload.data == {"value": value, "vote": vote} - - -class TestDummySynchronizedData: # pylint: disable=too-few-public-methods - """Test class for `DummySynchronizedData`.""" - - @staticmethod - @given(st.lists(st.text(max_size=200), max_size=100)) - def test_most_voted_keeper_address( - most_voted_keeper_address_data: List[str], - ) -> None: - """Test `most_voted_keeper_address`.""" - most_voted_keeper_address_key = "most_voted_keeper_address" - - dummy_synchronized_data = DummySynchronizedData( - db=AbciAppDB( - setup_data={ - most_voted_keeper_address_key: most_voted_keeper_address_data - } - ) - ) - - if len(most_voted_keeper_address_data) == 0: - with pytest.raises( - ValueError, - match=re.escape( - f"'{most_voted_keeper_address_key}' " - "field is not set for this period [0] and no default value was provided.", - ), - ): - _ = dummy_synchronized_data.most_voted_keeper_address - return - - assert ( - dummy_synchronized_data.most_voted_keeper_address - == most_voted_keeper_address_data[-1] - ) - - -class TestBaseRoundTestClass: - """Test `BaseRoundTestClass`.""" - - @staticmethod - def test_test_no_majority_event() -> None: - """Test `_test_no_majority_event`.""" - base_round_test = BaseRoundTestClass() - base_round_test._event_class = DummyEvent # pylint: disable=protected-access - - base_round_test._test_no_majority_event( # pylint: disable=protected-access - MagicMock( - end_block=lambda: ( - MagicMock(), - DummyEvent.NO_MAJORITY, - ) - ) - ) - - @staticmethod - @given(st.integers(min_value=0, max_value=100), st.integers(min_value=1)) - def test_complete_run(iter_count: int, shift: int) -> None: - """Test `_complete_run`.""" - - def dummy_gen() -> Generator[MagicMock, None, None]: - """A dummy generator.""" - return (MagicMock() for _ in range(iter_count)) - - # test with the same number as the generator's contents - gen = dummy_gen() - BaseRoundTestClass._complete_run( # pylint: disable=protected-access - gen, iter_count - ) - - # assert that the generator has been fully consumed - with pytest.raises(StopIteration): - next(gen) - - # test with a larger count than a generator's - with pytest.raises(StopIteration): - BaseRoundTestClass._complete_run( # pylint: disable=protected-access - dummy_gen(), iter_count + shift - ) - - -class BaseTestBase: - """Base class for the Base tests.""" - - gen: Generator - base_round_test: BaseRoundTestClass - base_round_test_cls: Type[BaseRoundTestClass] - test_method_name = "_test_round" - - def setup(self) -> None: - """Setup that is run before each test.""" - self.base_round_test = self.base_round_test_cls() - self.base_round_test._synchronized_data_class = ( # pylint: disable=protected-access - DummySynchronizedData - ) - self.base_round_test.setup() - self.base_round_test._event_class = ( # pylint: disable=protected-access - DummyEvent - ) - - def create_test_gen(self, **kwargs: Any) -> None: - """Create the base test generator.""" - test_method = getattr(self.base_round_test, self.test_method_name) - self.gen = test_method(**kwargs) - - def exhaust_base_test_gen(self) -> None: - """Exhaust the base test generator.""" - for _ in range(BASE_TEST_GEN_ITERATIONS): - next(self.gen) - last_iteration(self.gen) - - def run_test(self, **kwargs: Any) -> None: - """Run a test for a base test.""" - self.create_test_gen(**kwargs) - self.exhaust_base_test_gen() - - -class DummyCollectDifferentUntilAllRoundWithEndBlock( - DummyCollectDifferentUntilAllRound -): - """A `DummyCollectDifferentUntilAllRound` with `end_block` implemented.""" - - def __init__(self, dummy_exit_event: DummyEvent, *args: Any, **kwargs: Any): - """Initialize the dummy class.""" - super().__init__(*args, **kwargs) - self.dummy_exit_event = dummy_exit_event - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: - """A dummy `end_block` implementation.""" - if self.collection_threshold_reached and self.dummy_exit_event is not None: - return ( - cast( - DummySynchronizedData, - self.synchronized_data.update( - most_voted_keeper_address=list(self.collection.keys()) - ), - ), - self.dummy_exit_event, - ) - return None - - -class TestBaseCollectDifferentUntilAllRoundTest(BaseTestBase): - """Test `BaseCollectDifferentUntilAllRoundTest`.""" - - base_round_test: BaseCollectDifferentUntilAllRoundTest - base_round_test_cls = BaseCollectDifferentUntilAllRoundTest - - @given( - st.one_of(st.none(), st.sampled_from(DummyEvent)), - ) - def test_test_round(self, exit_event: DummyEvent) -> None: - """Test `_test_round`.""" - test_round = DummyCollectDifferentUntilAllRoundWithEndBlock( - exit_event, - self.base_round_test.synchronized_data, - context=MagicMock(), - ) - round_payloads = [ - DummyTxPayload(f"agent_{i}", str(i)) for i in range(MAX_PARTICIPANTS) - ] - synchronized_data_attr_checks = [ - lambda _synchronized_data: _synchronized_data.most_voted_keeper_address - ] - - self.run_test( - test_round=test_round, - round_payloads=round_payloads, - synchronized_data_update_fn=lambda synchronized_data, _: synchronized_data.update( - most_voted_keeper_address=[ - f"agent_{i}" for i in range(MAX_PARTICIPANTS) - ] - ), - synchronized_data_attr_checks=synchronized_data_attr_checks, - exit_event=exit_event, - ) - - -class DummyCollectSameUntilAllRoundWithEndBlock(DummyCollectSameUntilAllRound): - """A `DummyCollectSameUntilAllRound` with `end_block` implemented.""" - - def __init__(self, dummy_exit_event: DummyEvent, *args: Any, **kwargs: Any): - """Initialize the dummy class.""" - super().__init__(*args, **kwargs) - self.dummy_exit_event = dummy_exit_event - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: - """A dummy `end_block` implementation.""" - if self.collection_threshold_reached: - return ( - cast( - DummySynchronizedData, - self.synchronized_data.update( - most_voted_keeper_address=self.common_payload - ), - ), - self.dummy_exit_event, - ) - return None - - -class TestBaseCollectSameUntilAllRoundTest(BaseTestBase): - """Test `BaseCollectSameUntilAllRoundTest`.""" - - base_round_test: BaseCollectSameUntilAllRoundTest - base_round_test_cls: Type[ - BaseCollectSameUntilAllRoundTest - ] = BaseCollectSameUntilAllRoundTest - - @given( - st.sampled_from(DummyEvent), - st.text(max_size=500), - st.booleans(), - ) - def test_test_round( - self, exit_event: DummyEvent, common_value: str, finished: bool - ) -> None: - """Test `_test_round`.""" - test_round = DummyCollectSameUntilAllRoundWithEndBlock( - exit_event, - self.base_round_test.synchronized_data, - context=MagicMock(), - ) - round_payloads = { - f"test{i}": DummyTxPayload(f"agent_{i}", common_value) - for i in range(MAX_PARTICIPANTS) - } - synchronized_data_attr_checks = [ - lambda _synchronized_data: _synchronized_data.most_voted_keeper_address - ] - - self.run_test( - test_round=test_round, - round_payloads=round_payloads, - synchronized_data_update_fn=lambda synchronized_data, _: synchronized_data.update( - most_voted_keeper_address=common_value - ), - synchronized_data_attr_checks=synchronized_data_attr_checks, - most_voted_payload=common_value, - exit_event=exit_event, - finished=finished, - ) - - -class DummyCollectSameUntilThresholdRoundWithEndBlock( - DummyCollectSameUntilThresholdRound -): - """A `DummyCollectSameUntilThresholdRound` with `end_block` overriden.""" - - def __init__(self, dummy_exit_event: DummyEvent, *args: Any, **kwargs: Any): - """Initialize the dummy class.""" - super().__init__(*args, **kwargs) - self.dummy_exit_event = dummy_exit_event - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: - """A dummy `end_block` override.""" - if self.threshold_reached: - return ( - cast( - DummySynchronizedData, - self.synchronized_data.update( - most_voted_keeper_address=self.most_voted_payload - ), - ), - self.dummy_exit_event, - ) - if not self.is_majority_possible( - self.collection, self.synchronized_data.nb_participants - ): - return self.synchronized_data, DummyEvent.NO_MAJORITY - return None - - -class TestBaseCollectSameUntilThresholdRoundTest(BaseTestBase): - """Test `BaseCollectSameUntilThresholdRoundTest`.""" - - base_round_test: BaseCollectSameUntilThresholdRoundTest - base_round_test_cls: Type[ - BaseCollectSameUntilThresholdRoundTest - ] = BaseCollectSameUntilThresholdRoundTest - - @given( - st.sampled_from(DummyEvent), - st.text(max_size=500), - ) - def test_test_round(self, exit_event: DummyEvent, most_voted_payload: str) -> None: - """Test `_test_round`.""" - test_round = DummyCollectSameUntilThresholdRoundWithEndBlock( - exit_event, - self.base_round_test.synchronized_data, - context=MagicMock(), - ) - round_payloads = { - f"test{i}": DummyTxPayload(f"agent_{i}", most_voted_payload) - for i in range(MAX_PARTICIPANTS) - } - synchronized_data_attr_checks = [ - lambda _synchronized_data: _synchronized_data.most_voted_keeper_address - ] - - self.run_test( - test_round=test_round, - round_payloads=round_payloads, - synchronized_data_update_fn=lambda synchronized_data, _: synchronized_data.update( - most_voted_keeper_address=most_voted_payload - ), - synchronized_data_attr_checks=synchronized_data_attr_checks, - most_voted_payload=most_voted_payload, - exit_event=exit_event, - ) - - -class DummyOnlyKeeperSendsRoundTest(DummyOnlyKeeperSendsRound): - """A `DummyOnlyKeeperSendsRound` with `end_block` implemented.""" - - def __init__(self, dummy_exit_event: DummyEvent, *args: Any, **kwargs: Any): - """Initialize the dummy class.""" - super().__init__(*args, **kwargs) - self.dummy_exit_event = dummy_exit_event - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: - """A dummy `end_block` implementation.""" - if self.keeper_payload is not None and any( - [val is not None for val in self.keeper_payload.values] - ): - return ( - cast( - DummySynchronizedData, - self.synchronized_data.update( - blacklisted_keepers=self.keeper_payload.values[0] - ), - ), - self.dummy_exit_event, - ) - return None - - -class TestBaseOnlyKeeperSendsRoundTest(BaseTestBase): - """Test `BaseOnlyKeeperSendsRoundTest`.""" - - base_round_test: BaseOnlyKeeperSendsRoundTest - base_round_test_cls: Type[ - BaseOnlyKeeperSendsRoundTest - ] = BaseOnlyKeeperSendsRoundTest - most_voted_keeper_address: str = "agent_0" - - def setup(self) -> None: - """Setup that is run before each test.""" - super().setup() - self.base_round_test.synchronized_data.update( - most_voted_keeper_address=self.most_voted_keeper_address - ) - - @given( - st.sampled_from(DummyEvent), - st.text(), - ) - def test_test_round(self, exit_event: DummyEvent, keeper_value: str) -> None: - """Test `_test_round`.""" - test_round = DummyOnlyKeeperSendsRoundTest( - exit_event, - self.base_round_test.synchronized_data, - context=MagicMock(), - ) - keeper_payload = DummyTxPayload(self.most_voted_keeper_address, keeper_value) - synchronized_data_attr_checks = [ - lambda _synchronized_data: _synchronized_data.blacklisted_keepers - ] - - self.run_test( - test_round=test_round, - keeper_payloads=keeper_payload, - synchronized_data_update_fn=lambda synchronized_data, _: synchronized_data.update( - blacklisted_keepers=keeper_value - ), - synchronized_data_attr_checks=synchronized_data_attr_checks, - exit_event=exit_event, - ) - - -class DummyBaseVotingRoundTestWithEndBlock(DummyVotingRound): - """A `DummyVotingRound` with `end_block` overriden.""" - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: - """A dummy `end_block` override.""" - if self.positive_vote_threshold_reached: - synchronized_data = cast( - DummySynchronizedData, - self.synchronized_data.update( - is_keeper_set=bool(self.collection), - ), - ) - return synchronized_data, DummyEvent.DONE - if self.negative_vote_threshold_reached: - return self.synchronized_data, DummyEvent.NEGATIVE - if self.none_vote_threshold_reached: - return self.synchronized_data, DummyEvent.NONE - if not self.is_majority_possible( - self.collection, self.synchronized_data.nb_participants - ): - return self.synchronized_data, DummyEvent.NO_MAJORITY - return None - - -class TestBaseVotingRoundTest(BaseTestBase): - """Test `BaseVotingRoundTest`.""" - - base_round_test: BaseVotingRoundTest - base_round_test_cls: Type[BaseVotingRoundTest] = BaseVotingRoundTest - - @given( - st.one_of(st.none(), st.booleans()), - ) - def test_test_round(self, is_keeper_set: Optional[bool]) -> None: - """Test `_test_round`.""" - if is_keeper_set is None: - exit_event = DummyEvent.NONE - self.test_method_name = "_test_voting_round_none" - elif is_keeper_set: - exit_event = DummyEvent.DONE - self.test_method_name = "_test_voting_round_positive" - else: - exit_event = DummyEvent.NEGATIVE - self.test_method_name = "_test_voting_round_negative" - - test_round = DummyBaseVotingRoundTestWithEndBlock( - self.base_round_test.synchronized_data, - context=MagicMock(), - ) - round_payloads = { - f"test{i}": DummyTxPayload(f"agent_{i}", value="", vote=is_keeper_set) - for i in range(MAX_PARTICIPANTS) - } - synchronized_data_attr_checks = [ - lambda _synchronized_data: _synchronized_data.is_keeper_set - ] - - self.run_test( - test_round=test_round, - round_payloads=round_payloads, - synchronized_data_update_fn=lambda synchronized_data, _: synchronized_data.update( - is_keeper_set=is_keeper_set - ), - synchronized_data_attr_checks=synchronized_data_attr_checks, - exit_event=exit_event, - ) - - -class DummyCollectDifferentUntilThresholdRoundWithEndBlock( - DummyCollectDifferentUntilThresholdRound -): - """A `DummyCollectDifferentUntilThresholdRound` with `end_block` implemented.""" - - def __init__(self, dummy_exit_event: DummyEvent, *args: Any, **kwargs: Any): - """Initialize the dummy class.""" - super().__init__(*args, **kwargs) - self.dummy_exit_event = dummy_exit_event - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: - """A dummy `end_block` implementation.""" - if self.collection_threshold_reached and self.dummy_exit_event is not None: - return ( - cast( - DummySynchronizedData, - self.synchronized_data.update( - most_voted_keeper_address=list(self.collection.keys()) - ), - ), - self.dummy_exit_event, - ) - return None - - -class TestBaseCollectDifferentUntilThresholdRoundTest(BaseTestBase): - """Test `BaseCollectDifferentUntilThresholdRoundTest`.""" - - base_round_test: BaseCollectDifferentUntilThresholdRoundTest - base_round_test_cls: Type[ - BaseCollectDifferentUntilThresholdRoundTest - ] = BaseCollectDifferentUntilThresholdRoundTest - - @given(st.sampled_from(DummyEvent)) - def test_test_round(self, exit_event: DummyEvent) -> None: - """Test `_test_round`.""" - test_round = DummyCollectDifferentUntilThresholdRoundWithEndBlock( - exit_event, - self.base_round_test.synchronized_data, - context=MagicMock(), - ) - round_payloads = { - f"test{i}": DummyTxPayload(f"agent_{i}", str(i)) - for i in range(MAX_PARTICIPANTS) - } - synchronized_data_attr_checks = [ - lambda _synchronized_data: _synchronized_data.most_voted_keeper_address - ] - - self.run_test( - test_round=test_round, - round_payloads=round_payloads, - synchronized_data_update_fn=lambda synchronized_data, _: synchronized_data.update( - most_voted_keeper_address=[ - f"agent_{i}" for i in range(MAX_PARTICIPANTS) - ] - ), - synchronized_data_attr_checks=synchronized_data_attr_checks, - exit_event=exit_event, - ) diff --git a/packages/valory/skills/ipfs_package_downloader/__init__.py b/packages/valory/skills/ipfs_package_downloader/__init__.py deleted file mode 100644 index bb1ff74..0000000 --- a/packages/valory/skills/ipfs_package_downloader/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the implementation of the task execution skill.""" - -from aea.configurations.base import PublicId - - -PUBLIC_ID = PublicId.from_str("valory/ipfs_package_downloader:0.1.0") diff --git a/packages/valory/skills/ipfs_package_downloader/behaviours.py b/packages/valory/skills/ipfs_package_downloader/behaviours.py deleted file mode 100644 index 67cf594..0000000 --- a/packages/valory/skills/ipfs_package_downloader/behaviours.py +++ /dev/null @@ -1,215 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2023-2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains the implementation of a custom component's management.""" - -import time -from asyncio import Future -from typing import Any, Callable, Dict, Optional, Tuple, cast - -import yaml -from aea.protocols.base import Message -from aea.protocols.dialogue.base import Dialogue -from aea.skills.behaviours import SimpleBehaviour - -from packages.valory.connections.ipfs.connection import IpfsDialogues -from packages.valory.connections.ipfs.connection import PUBLIC_ID as IPFS_CONNECTION_ID -from packages.valory.protocols.ipfs import IpfsMessage -from packages.valory.protocols.ipfs.dialogues import IpfsDialogue -from packages.valory.skills.ipfs_package_downloader.models import Params - - -COMPONENT_YAML_STORE_KEY = "component_yaml" -ENTRY_POINT_STORE_KEY = "entry_point" -CALLABLES_STORE_KEY = "callables" - - -class IpfsPackageDownloader(SimpleBehaviour): - """A class to download packages from IPFS.""" - - def __init__(self, **kwargs: Any): - """Initialise the agent.""" - super().__init__(**kwargs) - self._executing_task: Optional[Dict[str, Optional[float]]] = None - self._packages_to_file_hash: Dict[str, str] = {} - self._all_packages: Dict[str, Dict[str, str]] = {} - self._inflight_package_req: Optional[str] = None - self._last_polling: Optional[float] = None - self._invalid_request = False - self._async_result: Optional[Future] = None - - def setup(self) -> None: - """Implement the setup.""" - self.context.logger.info("Setting up IpfsPackageDownloader") - self._packages_to_file_hash = { - value: key - for key, values in self.params.file_hash_to_id.items() - for value in values - } - - def act(self) -> None: - """Implement the act.""" - self._download_packages() - - @property - def params(self) -> Params: - """Get the parameters.""" - return cast(Params, self.context.params) - - @property - def request_id_to_num_timeouts(self) -> Dict[int, int]: - """Maps the request id to the number of times it has timed out.""" - return self.params.request_id_to_num_timeouts - - def count_timeout(self, request_id: int) -> None: - """Increase the timeout for a request.""" - self.request_id_to_num_timeouts[request_id] += 1 - - def timeout_limit_reached(self, request_id: int) -> bool: - """Check if the timeout limit has been reached.""" - return self.params.timeout_limit <= self.request_id_to_num_timeouts[request_id] - - def _has_executing_task_timed_out(self) -> bool: - """Check if the executing task timed out.""" - if self._executing_task is None: - return False - timeout_deadline = self._executing_task.get("timeout_deadline", None) - if timeout_deadline is None: - return False - return timeout_deadline <= time.time() - - def _download_packages(self) -> None: - """Download packages.""" - if self._inflight_package_req is not None: - # there already is a req in flight - return - if len(self._packages_to_file_hash) == len(self._all_packages): - # we already have all the packages - return - for package, file_hash in self._packages_to_file_hash.items(): - if package in self._all_packages: - continue - # read one at a time - ipfs_msg, message = self._build_ipfs_get_file_req(file_hash) - self._inflight_package_req = package - self.send_message(ipfs_msg, message, self._handle_get_package) - return - - def load_custom_component( - self, serialized_objects: Dict[str, str] - ) -> Dict[str, Any]: - """Load a custom component package. - - :param serialized_objects: the serialized objects. - :return: the component.yaml, entry_point.py and callable as tuple. - """ - # the package MUST contain a component.yaml file - if self.params.component_yaml_filename not in serialized_objects: - self.context.logger.error( - "Invalid component package. " - f"The package MUST contain a {self.params.component_yaml_filename}." - ) - return {} - # load the component.yaml file - component_yaml = yaml.safe_load( - serialized_objects[self.params.component_yaml_filename] - ) - if self.params.entry_point_key not in component_yaml or not all( - callable_key in component_yaml for callable_key in self.params.callable_keys - ): - self.context.logger.error( - f"Invalid component package. The {self.params.component_yaml_filename} file MUST contain the " - f"{self.params.entry_point_key} and {self.params.callable_keys} keys." - ) - return {} - # the name of the script that needs to be executed - entry_point_name = component_yaml[self.params.entry_point_key] - # load the script - if entry_point_name not in serialized_objects: - self.context.logger.error( - f"Invalid component package. " - f"The entry point {entry_point_name!r} is not present in the component package." - ) - return {} - entry_point = serialized_objects[entry_point_name] - # initialize with the methods that need to be called - component = { - callable_key: component_yaml[callable_key] - for callable_key in self.params.callable_keys - } - component.update( - { - COMPONENT_YAML_STORE_KEY: component_yaml, - ENTRY_POINT_STORE_KEY: entry_point, - } - ) - return component - - def _handle_get_package(self, message: IpfsMessage, _dialogue: Dialogue) -> None: - """Handle get package response""" - package_req = cast(str, self._inflight_package_req) - self._all_packages[package_req] = message.files - self.context.shared_state[package_req] = self.load_custom_component( - message.files - ) - self._inflight_package_req = None - - def send_message( - self, msg: Message, dialogue: Dialogue, callback: Callable - ) -> None: - """Send message.""" - self.context.outbox.put_message(message=msg) - nonce = dialogue.dialogue_label.dialogue_reference[0] - self.params.req_to_callback[nonce] = callback - self.params.in_flight_req = True - - def _build_ipfs_message( - self, - performative: IpfsMessage.Performative, - timeout: Optional[float] = None, - **kwargs: Any, - ) -> Tuple[IpfsMessage, IpfsDialogue]: - """Builds an IPFS message.""" - ipfs_dialogues = cast(IpfsDialogues, self.context.ipfs_dialogues) - message, dialogue = ipfs_dialogues.create( - counterparty=str(IPFS_CONNECTION_ID), - performative=performative, - timeout=timeout, - **kwargs, - ) - return message, dialogue - - def _build_ipfs_get_file_req( - self, - ipfs_hash: str, - timeout: Optional[float] = None, - ) -> Tuple[IpfsMessage, IpfsDialogue]: - """ - Builds a GET_FILES IPFS request. - - :param ipfs_hash: the ipfs hash of the file/dir to download. - :param timeout: timeout for the request. - :returns: the ipfs message, and its corresponding dialogue. - """ - message, dialogue = self._build_ipfs_message( - performative=IpfsMessage.Performative.GET_FILES, # type: ignore - ipfs_hash=ipfs_hash, - timeout=timeout, - ) - return message, dialogue diff --git a/packages/valory/skills/ipfs_package_downloader/dialogues.py b/packages/valory/skills/ipfs_package_downloader/dialogues.py deleted file mode 100644 index 884512c..0000000 --- a/packages/valory/skills/ipfs_package_downloader/dialogues.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ -"""This module contains dialogues.""" - -from typing import Any - -from aea.common import Address -from aea.protocols.base import Message -from aea.protocols.dialogue.base import Dialogue as BaseDialogue -from aea.skills.base import Model - -from packages.valory.protocols.ipfs.dialogues import IpfsDialogue as BaseIpfsDialogue -from packages.valory.protocols.ipfs.dialogues import IpfsDialogues as BaseIpfsDialogues - - -IpfsDialogue = BaseIpfsDialogue - - -class IpfsDialogues(Model, BaseIpfsDialogues): - """A class to keep track of IPFS dialogues.""" - - def __init__(self, **kwargs: Any) -> None: - """ - Initialize dialogues. - - :param kwargs: keyword arguments - """ - Model.__init__(self, **kwargs) - - def role_from_first_message( # pylint: disable=unused-argument - message: Message, receiver_address: Address - ) -> BaseDialogue.Role: - """Infer the role of the agent from an incoming/outgoing first message - - :param message: an incoming/outgoing first message - :param receiver_address: the address of the receiving agent - :return: The role of the agent - """ - return IpfsDialogue.Role.SKILL - - BaseIpfsDialogues.__init__( - self, - self_address=str(self.skill_id), - role_from_first_message=role_from_first_message, - ) diff --git a/packages/valory/skills/ipfs_package_downloader/handlers.py b/packages/valory/skills/ipfs_package_downloader/handlers.py deleted file mode 100644 index 1cad489..0000000 --- a/packages/valory/skills/ipfs_package_downloader/handlers.py +++ /dev/null @@ -1,89 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains a scaffold of a handler.""" - -from typing import cast - -from aea.protocols.base import Message -from aea.skills.base import Handler - -from packages.valory.protocols.ipfs import IpfsMessage -from packages.valory.skills.ipfs_package_downloader.models import Params - - -class BaseHandler(Handler): - """Base Handler""" - - def setup(self) -> None: - """Set up the handler.""" - self.context.logger.info(f"{self.__class__.__name__}: setup method called.") - - def cleanup_dialogues(self) -> None: - """Clean up all dialogues.""" - for handler_name in self.context.handlers.__dict__.keys(): - dialogues_name = handler_name.replace("_handler", "_dialogues") - dialogues = getattr(self.context, dialogues_name) - dialogues.cleanup() - - @property - def params(self) -> Params: - """Get the parameters.""" - return cast(Params, self.context.params) - - def teardown(self) -> None: - """Teardown the handler.""" - self.context.logger.info(f"{self.__class__.__name__}: teardown called.") - - def on_message_handled(self, _message: Message) -> None: - """Callback after a message has been handled.""" - self.params.request_count += 1 - if self.params.request_count % self.params.cleanup_freq == 0: - self.context.logger.info( - f"{self.params.request_count} requests processed. Cleaning up dialogues." - ) - self.cleanup_dialogues() - - -class IpfsHandler(BaseHandler): - """IPFS API message handler.""" - - SUPPORTED_PROTOCOL = IpfsMessage.protocol_id - - def handle(self, message: Message) -> None: - """ - Implement the reaction to an IPFS message. - - :param message: the message - """ - self.context.logger.info(f"Received message: {message}") - ipfs_msg = cast(IpfsMessage, message) - if ipfs_msg.performative == IpfsMessage.Performative.ERROR: - self.context.logger.warning( - f"IPFS Message performative not recognized: {ipfs_msg.performative}" - ) - self.params.in_flight_req = False - return - - dialogue = self.context.ipfs_dialogues.update(ipfs_msg) - nonce = dialogue.dialogue_label.dialogue_reference[0] - callback = self.params.req_to_callback.pop(nonce) - callback(ipfs_msg, dialogue) - self.params.in_flight_req = False - self.on_message_handled(message) diff --git a/packages/valory/skills/ipfs_package_downloader/models.py b/packages/valory/skills/ipfs_package_downloader/models.py deleted file mode 100644 index c445dbb..0000000 --- a/packages/valory/skills/ipfs_package_downloader/models.py +++ /dev/null @@ -1,68 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the shared state for the skill.""" -from collections import defaultdict -from typing import Any, Callable, Dict, List, cast - -from aea.exceptions import enforce -from aea.skills.base import Model - - -class Params(Model): - """A model to represent params for multiple abci apps.""" - - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Initialize the parameters object.""" - - self.in_flight_req: bool = False - self.req_to_callback: Dict[str, Callable] = {} - self.file_hash_to_id: Dict[ - str, List[str] - ] = self._nested_list_todict_workaround( - kwargs, - "file_hash_to_id", - ) - self.request_count: int = 0 - self.cleanup_freq = kwargs.get("cleanup_freq", 50) - self.timeout_limit = kwargs.get("timeout_limit", None) - self.component_yaml_filename = kwargs.get("component_yaml_filename", None) - self.entry_point_key = kwargs.get("entry_point_key", None) - self.callable_keys = kwargs.get("callable_keys", None) - enforce(self.timeout_limit is not None, "'timeout_limit' must be set!") - enforce( - self.component_yaml_filename is not None, - "'component_yaml_filename' must be set!", - ) - enforce(self.entry_point_key is not None, "'entry_point_key' must be set!") - enforce(self.callable_keys is not None, "'callable_keys' must be set!") - # maps the request id to the number of times it has timed out - self.request_id_to_num_timeouts: Dict[int, int] = defaultdict(lambda: 0) - super().__init__(*args, **kwargs) - - def _nested_list_todict_workaround( - self, - kwargs: Dict, - key: str, - ) -> Dict: - """Get a nested list from the kwargs and convert it to a dictionary.""" - values = cast(List, kwargs.get(key)) - if len(values) == 0: - raise ValueError(f"No {key} specified!") - return {value[0]: value[1] for value in values} diff --git a/packages/valory/skills/ipfs_package_downloader/skill.yaml b/packages/valory/skills/ipfs_package_downloader/skill.yaml deleted file mode 100644 index d68da28..0000000 --- a/packages/valory/skills/ipfs_package_downloader/skill.yaml +++ /dev/null @@ -1,57 +0,0 @@ -name: ipfs_package_downloader -author: valory -version: 0.1.0 -type: skill -description: A skill used for monitoring and executing tasks. -license: Apache-2.0 -aea_version: '>=1.0.0, <2.0.0' -fingerprint: - __init__.py: bafybeiaqd27il3osn4novyzipdtql4p36mnrskk2dqdsqthgjnb4gqixii - behaviours.py: bafybeig3lvx2viwoqynctj4wfusgx6h2qd7xchp6bfbfgnxq7xolm3mdhe - dialogues.py: bafybeibdxvhpzzqn4qa7us6cpkmtliwl7qziodt2srnicrdamfcev2mumi - handlers.py: bafybeieuf3jxlnzfs4yifwuvklzc2c7ps7kf4bmnzbd54ofz3q7jhpfgqy - models.py: bafybeibqo32ohd6o6au5lspvbnvkyptctegq6gto5jgz4mk6rj673fghf4 - utils/__init__.py: bafybeige5xuo32v4dykt6shggs452eliha5e5qzci5dwr6uowash3fq23q - utils/ipfs.py: bafybeihjb237abhcjupmmyswvfk7xmzgx3d4iufixes7v3yjmlycnca4xm - utils/task.py: bafybeiggyjn23wtzahdvq447jjwzmwn3hs2bc4sxkiy3wuzkugp35uoysq -fingerprint_ignore_patterns: [] -connections: -- valory/ipfs:0.1.0:bafybeiefkqvh5ylbk77xylcmshyuafmiecopt4gvardnubq52psvogis6a -contracts: [] -protocols: -- valory/ipfs:0.1.0:bafybeiftxi2qhreewgsc5wevogi7yc5g6hbcbo4uiuaibauhv3nhfcdtvm -skills: [] -behaviours: - ipfs_package_downloader: - args: {} - class_name: IpfsPackageDownloader -handlers: - ipfs_handler: - args: {} - class_name: IpfsHandler -models: - ipfs_dialogues: - args: {} - class_name: IpfsDialogues - params: - args: - cleanup_freq: 50 - timeout_limit: 3 - file_hash_to_id: - - - bafybeiabkbfjjakf7gvewxk5gvyybqvecy3k2tipvntjcovng2sbt3dq5m - - - follow_trend_strategy - component_yaml_filename: component.yaml - entry_point_key: entry_point - callable_keys: - - run_callable - - transform_callable - - evaluate_callable - class_name: Params -dependencies: - py-multibase: - version: ==1.0.3 - py-multicodec: - version: ==0.2.1 - pyyaml: - version: <=6.0.1,>=3.10 -is_abstract: false diff --git a/packages/valory/skills/ipfs_package_downloader/utils/__init__.py b/packages/valory/skills/ipfs_package_downloader/utils/__init__.py deleted file mode 100644 index 0475359..0000000 --- a/packages/valory/skills/ipfs_package_downloader/utils/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ -"""This module contains helper classes.""" diff --git a/packages/valory/skills/ipfs_package_downloader/utils/ipfs.py b/packages/valory/skills/ipfs_package_downloader/utils/ipfs.py deleted file mode 100644 index 5ccb20c..0000000 --- a/packages/valory/skills/ipfs_package_downloader/utils/ipfs.py +++ /dev/null @@ -1,94 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ -"""This module contains helpers for IPFS interaction.""" -from typing import Any, Dict, Tuple - -import yaml -from aea.helpers.cid import CID -from multibase import multibase -from multicodec import multicodec - - -CID_PREFIX = "f01701220" - - -def get_ipfs_file_hash(data: bytes) -> str: - """Get hash from bytes""" - try: - return str(CID.from_string(data.decode())) - except Exception: # noqa - # if something goes wrong, fallback to sha256 - file_hash = data.hex() - file_hash = CID_PREFIX + file_hash - file_hash = str(CID.from_string(file_hash)) - return file_hash - - -def to_multihash(hash_string: str) -> bytes: - """To multihash string.""" - # Decode the Base32 CID to bytes - cid_bytes = multibase.decode(hash_string) - # Remove the multicodec prefix (0x01) from the bytes - multihash_bytes = multicodec.remove_prefix(cid_bytes) - # Convert the multihash bytes to a hexadecimal string - hex_multihash = multihash_bytes.hex() - return hex_multihash[6:] - - -class ComponentPackageLoader: - """Component package loader.""" - - @staticmethod - def load(serialized_objects: Dict[str, str]) -> Tuple[Dict[str, Any], str, str]: - """ - Load a custom component package. - - :param serialized_objects: the serialized objects. - :return: the component.yaml, entry_point.py and callable as tuple. - """ - # the package MUST contain a component.yaml file - if "component.yaml" not in serialized_objects: - raise ValueError( - "Invalid component package. " - "The package MUST contain a component.yaml." - ) - - # load the component.yaml file - component_yaml = yaml.safe_load(serialized_objects["component.yaml"]) - if "entry_point" not in component_yaml or "callable" not in component_yaml: - raise ValueError( - "Invalid component package. " - "The component.yaml file MUST contain the 'entry_point' and 'callable' keys." - ) - - # the name of the script that needs to be executed - entry_point_name = component_yaml["entry_point"] - - # load the script - if entry_point_name not in serialized_objects: - raise ValueError( - f"Invalid component package. " - f"{entry_point_name} is not present in the component package." - ) - entry_point = serialized_objects[entry_point_name] - - # the method that needs to be called - callable_method = component_yaml["callable"] - - return component_yaml, entry_point, callable_method diff --git a/packages/valory/skills/ipfs_package_downloader/utils/task.py b/packages/valory/skills/ipfs_package_downloader/utils/task.py deleted file mode 100644 index 8169a9c..0000000 --- a/packages/valory/skills/ipfs_package_downloader/utils/task.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains a custom Loader for the ipfs connection.""" - -from typing import Any - - -class AnyToolAsTask: - """AnyToolAsTask""" - - def execute(self, *args: Any, **kwargs: Any) -> Any: - """Execute the task.""" - tool_py = kwargs.pop("tool_py") - callable_method = kwargs.pop("callable_method") - if callable_method in globals(): - del globals()[callable_method] - exec(tool_py, globals()) # pylint: disable=W0122 # nosec - method = globals()[callable_method] - return method(*args, **kwargs) diff --git a/packages/valory/skills/market_data_fetcher_abci/__init__.py b/packages/valory/skills/market_data_fetcher_abci/__init__.py deleted file mode 100644 index 695275f..0000000 --- a/packages/valory/skills/market_data_fetcher_abci/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the implementation of the default skill.""" - -from aea.configurations.base import PublicId - - -PUBLIC_ID = PublicId.from_str("valory/market_data_fetcher_abci:0.1.0") diff --git a/packages/valory/skills/market_data_fetcher_abci/behaviours.py b/packages/valory/skills/market_data_fetcher_abci/behaviours.py deleted file mode 100644 index c382ad0..0000000 --- a/packages/valory/skills/market_data_fetcher_abci/behaviours.py +++ /dev/null @@ -1,449 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains round behaviours of MarketDataFetcherAbciApp.""" - -import json -import os -from abc import ABC -from datetime import datetime, timedelta -from typing import ( - Any, - Callable, - Dict, - Generator, - Optional, - Set, - Tuple, - Type, - Union, - cast, -) - -from packages.eightballer.connections.dcxt.connection import ( - PUBLIC_ID as DCXT_CONNECTION_ID, -) -from packages.eightballer.protocols.tickers.message import TickersMessage -from packages.valory.skills.abstract_round_abci.base import AbstractRound -from packages.valory.skills.abstract_round_abci.behaviour_utils import SOLANA_LEDGER_ID -from packages.valory.skills.abstract_round_abci.behaviours import ( - AbstractRoundBehaviour, - BaseBehaviour, -) -from packages.valory.skills.abstract_round_abci.io_.store import SupportedFiletype -from packages.valory.skills.market_data_fetcher_abci.models import Coingecko, Params -from packages.valory.skills.market_data_fetcher_abci.payloads import ( - TransformedMarketDataPayload, -) -from packages.valory.skills.market_data_fetcher_abci.rounds import ( - FetchMarketDataRound, - MarketDataFetcherAbciApp, - MarketDataPayload, - SynchronizedData, - TransformMarketDataRound, -) - - -HTTP_OK = [200, 201] -MAX_RETRIES = 3 -MARKETS_FILE_NAME = "markets.json" -TOKEN_ID_FIELD = "coingecko_id" # nosec: B105:hardcoded_password_string -TOKEN_ADDRESS_FIELD = "address" # nosec: B105:hardcoded_password_string -UTF8 = "utf-8" -STRATEGY_KEY = "trading_strategy" -ENTRY_POINT_STORE_KEY = "entry_point" -TRANSFORM_CALLABLE_STORE_KEY = "transform_callable" -DEFAULT_MARKET_SEPARATOR = "/" -DEFAULT_DATE_RANGE_SECONDS = 300 -DEFAULT_DATA_LOOKBACK_MINUTES = 60 -DEFAULT_DATA_VOLUME = 100 -SECONDS_TO_MILLISECONDS = 1000 - - -def date_range_generator( # type: ignore - start, end, seconds_delta=DEFAULT_DATE_RANGE_SECONDS -) -> Generator[int, None, None]: - """Generate a range of dates in the ms format. We do this as tickers are just spot prices and we dont yet retrieve historical data.""" - current = start - while current < end: - yield current.timestamp() * SECONDS_TO_MILLISECONDS - current += timedelta(seconds=seconds_delta) - - -class MarketDataFetcherBaseBehaviour(BaseBehaviour, ABC): - """Base behaviour for the market_data_fetcher_abci skill.""" - - @property - def synchronized_data(self) -> SynchronizedData: - """Return the synchronized data.""" - return cast(SynchronizedData, super().synchronized_data) - - @property - def params(self) -> Params: - """Return the params.""" - return cast(Params, super().params) - - @property - def coingecko(self) -> Coingecko: - """Return the Coingecko.""" - return cast(Coingecko, self.context.coingecko) - - def from_data_dir(self, path: str) -> str: - """Return the given path appended to the data dir.""" - return os.path.join(self.context.data_dir, path) - - def _request_with_retries( - self, - endpoint: str, - rate_limited_callback: Callable, - method: str = "GET", - body: Optional[Any] = None, - headers: Optional[Dict] = None, - rate_limited_code: int = 429, - max_retries: int = MAX_RETRIES, - retry_wait: int = 0, - ) -> Generator[None, None, Tuple[bool, Dict]]: - """Request wrapped around a retry mechanism""" - - self.context.logger.info(f"HTTP {method} call: {endpoint}") - content = json.dumps(body).encode(UTF8) if body else None - - retries = 0 - while True: - # Make the request - response = yield from self.get_http_response( - method, endpoint, content, headers - ) - - try: - response_json = json.loads(response.body) - except json.decoder.JSONDecodeError as exc: - self.context.logger.error(f"Exception during json loading: {exc}") - response_json = {"exception": str(exc)} - - if response.status_code == rate_limited_code: - rate_limited_callback() - return False, response_json - - if response.status_code not in HTTP_OK or "exception" in response_json: - self.context.logger.error( - f"Request failed [{response.status_code}]: {response_json}" - ) - retries += 1 - if retries == max_retries: - break - yield from self.sleep(retry_wait) - continue - - self.context.logger.info("Request succeeded.") - return True, response_json - - self.context.logger.error(f"Request failed after {retries} retries.") - return False, response_json - - def get_dcxt_response( - self, - protocol_performative: TickersMessage.Performative, - **kwargs: Any, - ) -> Generator[None, None, Any]: - """Get a ccxt response.""" - if protocol_performative not in self._performative_to_dialogue_class: - raise ValueError( - f"Unsupported protocol performative {protocol_performative:!r}" - ) - dialogue_class = self._performative_to_dialogue_class[protocol_performative] - - msg, dialogue = dialogue_class.create( - counterparty=str(DCXT_CONNECTION_ID), - performative=protocol_performative, - **kwargs, - ) - msg._sender = str(self.context.skill_id) # pylint: disable=protected-access - response = yield from self._do_request(msg, dialogue) - return response - - def __init__(self, **kwargs: Any): - """Initialize the behaviour.""" - super().__init__(**kwargs) - self._performative_to_dialogue_class = { - TickersMessage.Performative.GET_ALL_TICKERS: self.context.tickers_dialogues, - } - - -class FetchMarketDataBehaviour(MarketDataFetcherBaseBehaviour): - """FetchMarketDataBehaviour""" - - matching_round: Type[AbstractRound] = FetchMarketDataRound - - exchange_to_tickers: Dict[str, Any] = {} - - def async_act(self) -> Generator: - """Do the act, supporting asynchronous execution.""" - - with self.context.benchmark_tool.measure(self.behaviour_id).local(): - data_hash = yield from self.fetch_markets() - sender = self.context.agent_address - payload = MarketDataPayload(sender=sender, data_hash=data_hash) - - with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): - yield from self.send_a2a_transaction(payload) - yield from self.wait_until_round_end() - - self.set_done() - - def _fetch_solana_market_data( - self, - ) -> Generator[None, None, Dict[Any, Dict[str, Any]]]: - """Fetch Solana market data from Coingecko and send to IPFS""" - - headers = { - "Accept": "application/json", - } - if self.coingecko.api_key: - headers["x-cg-pro-api-key"] = self.coingecko.api_key - markets = {} - for token_data in self.params.token_symbol_whitelist: - token_id = token_data.get(TOKEN_ID_FIELD, None) - token_address = token_data.get(TOKEN_ADDRESS_FIELD, None) - - if not token_id or not token_address: - err = f"Token id or address missing in whitelist's {token_data=}." - self.context.logger.error(err) - continue - - warned = False - while not self.coingecko.rate_limiter.check_and_burn(): - if not warned: - self.context.logger.warning( - "Rate limiter activated. " - "To avoid this in the future, you may consider acquiring a Coingecko API key," - "and updating the `Coingecko` model's overrides.\n" - "Cooling down..." - ) - warned = True - yield from self.sleep(self.params.sleep_time) - - if warned: - self.context.logger.info("Cooldown period passed :)") - - remaining_limit = self.coingecko.rate_limiter.remaining_limit - remaining_credits = self.coingecko.rate_limiter.remaining_credits - self.context.logger.info( - "Local rate limiter's check passed. " - f"After the call, you will have {remaining_limit=} and {remaining_credits=}." - ) - success, response_json = yield from self._request_with_retries( - endpoint=self.coingecko.endpoint.format(token_id=token_id), - headers=headers, - rate_limited_code=self.coingecko.rate_limited_code, - rate_limited_callback=self.coingecko.rate_limited_status_callback, - retry_wait=self.params.sleep_time, - ) - - # Skip failed markets. The strategy will need to verify market availability - if not success: - self.context.logger.error( - f"Failed to fetch market data for {token_id}." - ) - continue - - self.context.logger.info( - f"Successfully fetched market data for {token_id}." - ) - # we collect a tuple of the prices and the volumes - - prices = response_json.get(self.coingecko.prices_field, []) - volumes = response_json.get(self.coingecko.volumes_field, []) - prices_volumes = {"prices": prices, "volumes": volumes} - markets[token_address] = prices_volumes - return markets - - def _fetch_dcxt_market_data( - self, ledger_id: str - ) -> Generator[None, None, Dict[Union[str, Any], Dict[str, object]]]: - params = { - "ledger_id": ledger_id, - } - for key, value in params.items(): - params[key] = value.encode("utf-8") # type: ignore - exchanges = self.params.exchange_ids[ledger_id] - - markets = {} - for exchange_id in exchanges: - msg: TickersMessage = yield from self.get_dcxt_response( - protocol_performative=TickersMessage.Performative.GET_ALL_TICKERS, # type: ignore - exchange_id=f"{exchange_id}_{ledger_id}", - params=params, - ) - self.context.logger.info( - f"Received {len(msg.tickers.tickers)} tickers from {exchange_id}" - ) - - for ticker in msg.tickers.tickers: - token_address = ticker.symbol.split(DEFAULT_MARKET_SEPARATOR)[0] # type: ignore - - dates = list( - date_range_generator( - datetime.now() - - timedelta(minutes=DEFAULT_DATA_LOOKBACK_MINUTES), - datetime.now(), - ) - ) - prices = [[date, ticker.ask] for date in dates] - volumes = [ - [ - date, - DEFAULT_DATA_VOLUME, # This is a placeholder for the volume - ] - for date in dates - ] - prices_volumes = {"prices": prices, "volumes": volumes} - markets[token_address] = prices_volumes - return markets - - def fetch_markets(self) -> Generator[None, None, Optional[str]]: - """Fetch markets from Coingecko and send to IPFS""" - - ledger_market_data: Dict[str, Dict[str, Any]] = {} - - # Get the market data for each token for each ledger_id - for ledger_id in self.params.ledger_ids: - self.context.logger.info(f"Fetching market data for {ledger_id}.") - if ledger_id == SOLANA_LEDGER_ID: - markets = yield from self._fetch_solana_market_data() - ledger_market_data.update(SOLANA_LEDGER_ID=markets) - else: - # We assume it is an EVM chain and thus route to dcxt. - markets = yield from self._fetch_dcxt_market_data(ledger_id) - ledger_market_data.update({ledger_id: markets}) - data_hash = None - if markets: - data_hash = yield from self.send_to_ipfs( - filename=self.from_data_dir(MARKETS_FILE_NAME), - obj=markets, - filetype=SupportedFiletype.JSON, - ) - self.context.logger.info( - f"Market file stored in IPFS. Hash is {data_hash}." - ) - - return data_hash - - -class TransformMarketDataBehaviour(MarketDataFetcherBaseBehaviour): - """Behaviour to transform the fetched signals.""" - - matching_round: Type[AbstractRound] = TransformMarketDataRound - - def strategy_store(self, strategy_name: str) -> Dict[str, str]: - """Get the stored strategy's files.""" - return self.context.shared_state.get(strategy_name, {}) - - def execute_strategy_transformation( - self, *args: Any, **kwargs: Any - ) -> Dict[str, Any] | None: - """Execute the strategy's transform method and return the results.""" - trading_strategy = kwargs.pop(STRATEGY_KEY, None) - if trading_strategy is None: - self.context.logger.error(f"No {STRATEGY_KEY!r} was given!") - return None - - store = self.strategy_store(trading_strategy) - strategy_exec = store.get(ENTRY_POINT_STORE_KEY, None) - if strategy_exec is None: - self.context.logger.error( - f"No executable was found for {trading_strategy=}! Did the IPFS package downloader load it correctly?" - ) - return None - - callable_method = store.get(TRANSFORM_CALLABLE_STORE_KEY, None) - if callable_method is None: - self.context.logger.error( - "No transform callable was found in the loaded component! " - "Did the IPFS package downloader load it correctly?" - ) - return None - - if callable_method in globals(): - del globals()[callable_method] - - exec(strategy_exec, globals()) # pylint: disable=W0122 # nosec - method = globals().get(callable_method, None) - if method is None: - self.context.logger.error( - f"No {callable_method!r} method was found in {trading_strategy} strategy's executable:\n" - f"{strategy_exec}." - ) - return None - return method(*args, **kwargs) - - def async_act(self) -> Generator: - """Do the act, supporting asynchronous execution.""" - - with self.context.benchmark_tool.measure(self.behaviour_id).local(): - data_hash = yield from self.transform_data() - sender = self.context.agent_address - payload = TransformedMarketDataPayload( - sender=sender, transformed_data_hash=data_hash - ) - - with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): - yield from self.send_a2a_transaction(payload) - yield from self.wait_until_round_end() - - self.set_done() - - def transform_data( - self, - ) -> Generator[None, None, Optional[str]]: - """Transform the data to OHLCV format.""" - markets_data = yield from self.get_from_ipfs( - self.synchronized_data.data_hash, SupportedFiletype.JSON - ) - markets_data = cast(Dict[str, Dict[str, Any]], markets_data) - results = {} - - strategy = self.synchronized_data.selected_strategy - for token_address, market_data in markets_data.items(): - kwargs = {STRATEGY_KEY: strategy, **market_data} - result = self.execute_strategy_transformation(**kwargs) - if result is None: - self.context.logger.error( - f"Failed to transform market data for {token_address}." - ) - continue - results[token_address] = result - - data_hash = yield from self.send_to_ipfs( - filename=self.from_data_dir(MARKETS_FILE_NAME), - obj=results, - filetype=SupportedFiletype.JSON, - ) - return data_hash - - -class MarketDataFetcherRoundBehaviour(AbstractRoundBehaviour): - """MarketDataFetcherRoundBehaviour""" - - initial_behaviour_cls = FetchMarketDataBehaviour - abci_app_cls = MarketDataFetcherAbciApp - behaviours: Set[Type[BaseBehaviour]] = { - FetchMarketDataBehaviour, # type: ignore - TransformMarketDataBehaviour, # type: ignore - } diff --git a/packages/valory/skills/market_data_fetcher_abci/dialogues.py b/packages/valory/skills/market_data_fetcher_abci/dialogues.py deleted file mode 100644 index ba3bcbd..0000000 --- a/packages/valory/skills/market_data_fetcher_abci/dialogues.py +++ /dev/null @@ -1,101 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the dialogues of the MarketDataFetcherAbciApp.""" - -from packages.eightballer.protocols.tickers.dialogues import ( - TickersDialogue as BaseTickersDialogue, -) -from packages.eightballer.protocols.tickers.dialogues import ( - TickersDialogues as BaseTickersDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - AbciDialogue as BaseAbciDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - AbciDialogues as BaseAbciDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - ContractApiDialogue as BaseContractApiDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - ContractApiDialogues as BaseContractApiDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - HttpDialogue as BaseHttpDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - HttpDialogues as BaseHttpDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - IpfsDialogue as BaseIpfsDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - IpfsDialogues as BaseIpfsDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - LedgerApiDialogue as BaseLedgerApiDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - LedgerApiDialogues as BaseLedgerApiDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - SigningDialogue as BaseSigningDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - SigningDialogues as BaseSigningDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - TendermintDialogue as BaseTendermintDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - TendermintDialogues as BaseTendermintDialogues, -) - - -AbciDialogue = BaseAbciDialogue -AbciDialogues = BaseAbciDialogues - - -HttpDialogue = BaseHttpDialogue -HttpDialogues = BaseHttpDialogues - - -SigningDialogue = BaseSigningDialogue -SigningDialogues = BaseSigningDialogues - - -LedgerApiDialogue = BaseLedgerApiDialogue -LedgerApiDialogues = BaseLedgerApiDialogues - - -ContractApiDialogue = BaseContractApiDialogue -ContractApiDialogues = BaseContractApiDialogues - - -TendermintDialogue = BaseTendermintDialogue -TendermintDialogues = BaseTendermintDialogues - - -IpfsDialogue = BaseIpfsDialogue -IpfsDialogues = BaseIpfsDialogues - - -TickersDialogue = BaseTickersDialogue -TickersDialogues = BaseTickersDialogues diff --git a/packages/valory/skills/market_data_fetcher_abci/fsm_specification.yaml b/packages/valory/skills/market_data_fetcher_abci/fsm_specification.yaml deleted file mode 100644 index b7d5fba..0000000 --- a/packages/valory/skills/market_data_fetcher_abci/fsm_specification.yaml +++ /dev/null @@ -1,26 +0,0 @@ -alphabet_in: -- DONE -- NONE -- NO_MAJORITY -- ROUND_TIMEOUT -default_start_state: FetchMarketDataRound -final_states: -- FailedMarketFetchRound -- FinishedMarketFetchRound -label: MarketDataFetcherAbciApp -start_states: -- FetchMarketDataRound -states: -- FailedMarketFetchRound -- FetchMarketDataRound -- FinishedMarketFetchRound -- TransformMarketDataRound -transition_func: - (FetchMarketDataRound, DONE): TransformMarketDataRound - (FetchMarketDataRound, NONE): FailedMarketFetchRound - (FetchMarketDataRound, NO_MAJORITY): FetchMarketDataRound - (FetchMarketDataRound, ROUND_TIMEOUT): FetchMarketDataRound - (TransformMarketDataRound, DONE): FinishedMarketFetchRound - (TransformMarketDataRound, NONE): FailedMarketFetchRound - (TransformMarketDataRound, NO_MAJORITY): TransformMarketDataRound - (TransformMarketDataRound, ROUND_TIMEOUT): TransformMarketDataRound diff --git a/packages/valory/skills/market_data_fetcher_abci/handlers.py b/packages/valory/skills/market_data_fetcher_abci/handlers.py deleted file mode 100644 index 2ae35dd..0000000 --- a/packages/valory/skills/market_data_fetcher_abci/handlers.py +++ /dev/null @@ -1,66 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the handlers for the skill of MarketDataFetcherAbciApp.""" - -from packages.eightballer.protocols.tickers.message import TickersMessage -from packages.valory.skills.abstract_round_abci.handlers import ( - ABCIRoundHandler as BaseABCIRoundHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import AbstractResponseHandler -from packages.valory.skills.abstract_round_abci.handlers import ( - ContractApiHandler as BaseContractApiHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - HttpHandler as BaseHttpHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - IpfsHandler as BaseIpfsHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - LedgerApiHandler as BaseLedgerApiHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - SigningHandler as BaseSigningHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - TendermintHandler as BaseTendermintHandler, -) - - -class DcxtTickersHandler(AbstractResponseHandler): - """This class implements a handler for DexTickersHandler messages.""" - - SUPPORTED_PROTOCOL = TickersMessage.protocol_id - allowed_response_performatives = frozenset( - { - TickersMessage.Performative.ALL_TICKERS, - TickersMessage.Performative.TICKER, - TickersMessage.Performative.ERROR, - } - ) - - -ABCIHandler = BaseABCIRoundHandler -HttpHandler = BaseHttpHandler -SigningHandler = BaseSigningHandler -LedgerApiHandler = BaseLedgerApiHandler -ContractApiHandler = BaseContractApiHandler -TendermintHandler = BaseTendermintHandler -IpfsHandler = BaseIpfsHandler diff --git a/packages/valory/skills/market_data_fetcher_abci/models.py b/packages/valory/skills/market_data_fetcher_abci/models.py deleted file mode 100644 index 6a3ac6e..0000000 --- a/packages/valory/skills/market_data_fetcher_abci/models.py +++ /dev/null @@ -1,187 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the shared state for the abci skill of MarketDataFetcherAbciApp.""" - -from datetime import datetime -from time import time -from typing import Any, Dict, List, Optional - -from aea.skills.base import Model - -from packages.valory.skills.abstract_round_abci.models import BaseParams -from packages.valory.skills.abstract_round_abci.models import ( - BenchmarkTool as BaseBenchmarkTool, -) -from packages.valory.skills.abstract_round_abci.models import Requests as BaseRequests -from packages.valory.skills.abstract_round_abci.models import ( - SharedState as BaseSharedState, -) -from packages.valory.skills.abstract_round_abci.models import TypeCheckMixin -from packages.valory.skills.market_data_fetcher_abci.rounds import ( - MarketDataFetcherAbciApp, -) - - -MINUTE_UNIX = 60 - - -def format_whitelist(token_whitelist: List) -> List: - """Load the token whitelist into its proper format""" - fixed_whitelist = [] - for element in token_whitelist: - token_config = {} - for i in element.split("&"): - key, value = i.split("=") - token_config[key] = value - fixed_whitelist.append(token_config) - return fixed_whitelist - - -class SharedState(BaseSharedState): - """Keep the current shared state of the skill.""" - - abci_app_cls = MarketDataFetcherAbciApp - - -class Params(BaseParams): - """Parameters.""" - - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Initialize the parameters object.""" - self.token_symbol_whitelist: List[Dict] = format_whitelist( - self._ensure("token_symbol_whitelist", kwargs, List[str]) - ) - self.ledger_ids = self._ensure("ledger_ids", kwargs, List[str]) - self.exchange_ids = self._ensure("exchange_ids", kwargs, Dict[str, List[str]]) - super().__init__(*args, **kwargs) - - -class CoingeckoRateLimiter: - """Keeps track of the rate limiting for Coingecko.""" - - def __init__(self, limit: int, credits_: int) -> None: - """Initialize the Coingecko rate limiter.""" - self._limit = self._remaining_limit = limit - self._credits = self._remaining_credits = credits_ - self._last_request_time = time() - - @property - def limit(self) -> int: - """Get the limit per minute.""" - return self._limit - - @property - def credits(self) -> int: - """Get the requests' cap per month.""" - return self._credits - - @property - def remaining_limit(self) -> int: - """Get the remaining limit per minute.""" - return self._remaining_limit - - @property - def remaining_credits(self) -> int: - """Get the remaining requests' cap per month.""" - return self._remaining_credits - - @property - def last_request_time(self) -> float: - """Get the timestamp of the last request.""" - return self._last_request_time - - @property - def rate_limited(self) -> bool: - """Check whether we are rate limited.""" - return self.remaining_limit == 0 - - @property - def no_credits(self) -> bool: - """Check whether all the credits have been spent.""" - return self.remaining_credits == 0 - - @property - def cannot_request(self) -> bool: - """Check whether we cannot perform a request.""" - return self.rate_limited or self.no_credits - - @property - def credits_reset_timestamp(self) -> int: - """Get the UNIX timestamp in which the Coingecko credits reset.""" - current_date = datetime.now() - first_day_of_next_month = datetime(current_date.year, current_date.month + 1, 1) - return int(first_day_of_next_month.timestamp()) - - @property - def can_reset_credits(self) -> bool: - """Check whether the Coingecko credits can be reset.""" - return self.last_request_time >= self.credits_reset_timestamp - - def _update_limits(self) -> None: - """Update the remaining limits and the credits if necessary.""" - time_passed = time() - self.last_request_time - limit_increase = int(time_passed / MINUTE_UNIX) * self.limit - self._remaining_limit = min(self.limit, self.remaining_limit + limit_increase) - if self.can_reset_credits: - self._remaining_credits = self.credits - - def _burn_credit(self) -> None: - """Use one credit.""" - self._remaining_limit -= 1 - self._remaining_credits -= 1 - self._last_request_time = time() - - def check_and_burn(self) -> bool: - """Check whether we can perform a new request, and if yes, update the remaining limit and credits.""" - self._update_limits() - if self.cannot_request: - return False - self._burn_credit() - return True - - -class Coingecko(Model, TypeCheckMixin): - """Coingecko configuration.""" - - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Initialize the Coingecko object.""" - self.endpoint: str = self._ensure("endpoint", kwargs, str) - self.api_key: Optional[str] = self._ensure("api_key", kwargs, Optional[str]) - self.prices_field: str = self._ensure("prices_field", kwargs, str) - self.volumes_field: str = self._ensure("volumes_field", kwargs, str) - self.rate_limited_code: int = self._ensure("rate_limited_code", kwargs, int) - limit: int = self._ensure("requests_per_minute", kwargs, int) - credits_: int = self._ensure("credits", kwargs, int) - self.rate_limiter = CoingeckoRateLimiter(limit, credits_) - super().__init__(*args, **kwargs) - - def rate_limited_status_callback(self) -> None: - """Callback when a rate-limited status is returned from the API.""" - self.context.logger.error( - "Unexpected rate-limited status code was received from the Coingecko API! " - "Setting the limit to 0 on the local rate limiter to partially address the issue. " - "Please check whether the `Coingecko` overrides are set corresponding to the API's rules." - ) - self.rate_limiter._remaining_limit = 0 - self.rate_limiter._last_request_time = time() - - -Requests = BaseRequests -BenchmarkTool = BaseBenchmarkTool diff --git a/packages/valory/skills/market_data_fetcher_abci/payloads.py b/packages/valory/skills/market_data_fetcher_abci/payloads.py deleted file mode 100644 index 3ccd890..0000000 --- a/packages/valory/skills/market_data_fetcher_abci/payloads.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the transaction payloads of the MarketDataFetcherAbciApp.""" - -from dataclasses import dataclass -from typing import Optional - -from packages.valory.skills.abstract_round_abci.base import BaseTxPayload - - -@dataclass(frozen=True) -class MarketDataPayload(BaseTxPayload): - """Represent a transaction payload for the market data.""" - - data_hash: Optional[str] - - -@dataclass(frozen=True) -class TransformedMarketDataPayload(BaseTxPayload): - """Represent a transaction payload for the market data.""" - - transformed_data_hash: Optional[str] diff --git a/packages/valory/skills/market_data_fetcher_abci/rounds.py b/packages/valory/skills/market_data_fetcher_abci/rounds.py deleted file mode 100644 index f47dbc5..0000000 --- a/packages/valory/skills/market_data_fetcher_abci/rounds.py +++ /dev/null @@ -1,175 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains the rounds of MarketDataFetcherAbciApp.""" - -from enum import Enum -from typing import Dict, FrozenSet, Set, Type - -from packages.valory.skills.abstract_round_abci.base import ( - AbciApp, - AbciAppTransitionFunction, - AppState, - BaseSynchronizedData, - BaseTxPayload, - CollectSameUntilThresholdRound, - CollectionRound, - DegenerateRound, - DeserializedCollection, - EventToTimeout, - get_name, -) -from packages.valory.skills.market_data_fetcher_abci.payloads import ( - MarketDataPayload, - TransformedMarketDataPayload, -) - - -class Event(Enum): - """MarketDataFetcherAbciApp Events""" - - DONE = "done" - NONE = "none" - NO_MAJORITY = "no_majority" - ROUND_TIMEOUT = "round_timeout" - - -class SynchronizedData(BaseSynchronizedData): - """ - Class to represent the synchronized data. - - This data is replicated by the tendermint application. - """ - - def _get_deserialized(self, key: str) -> DeserializedCollection: - """Strictly get a collection and return it deserialized.""" - serialized = self.db.get_strict(key) - return CollectionRound.deserialize_collection(serialized) - - @property - def data_hash(self) -> str: - """Get the hash of the tokens' data.""" - return str(self.db.get_strict("data_hash")) - - @property - def transformed_data_hash(self) -> str: - """Get the hash of the tokens' data.""" - return str(self.db.get_strict("transformed_data_hash")) - - @property - def participant_to_fetching(self) -> DeserializedCollection: - """Get the participants to market fetching.""" - return self._get_deserialized("participant_to_fetching") - - @property - def participant_to_transforming(self) -> DeserializedCollection: - """Get the participants to market data transformation.""" - return self._get_deserialized("participant_to_transforming") - - @property - def selected_strategy(self) -> str: - """Get the selected strategy.""" - return self.db.get_strict("selected_strategy") - - -class FetchMarketDataRound(CollectSameUntilThresholdRound): - """FetchMarketDataRound""" - - payload_class: Type[BaseTxPayload] = MarketDataPayload - synchronized_data_class = SynchronizedData - done_event = Event.DONE - none_event = Event.NONE - no_majority_event = Event.NO_MAJORITY - selection_key = get_name(SynchronizedData.data_hash) - collection_key = get_name(SynchronizedData.participant_to_fetching) - - -class TransformMarketDataRound(FetchMarketDataRound): - """Round to transform the fetched signals.""" - - payload_class = TransformedMarketDataPayload - selection_key = get_name(SynchronizedData.transformed_data_hash) - collection_key = get_name(SynchronizedData.participant_to_transforming) - - -class FinishedMarketFetchRound(DegenerateRound): - """FinishedMarketFetchRound""" - - -class FailedMarketFetchRound(DegenerateRound): - """FailedMarketFetchRound""" - - -class MarketDataFetcherAbciApp(AbciApp[Event]): - """MarketDataFetcherAbciApp - - Initial round: FetchMarketDataRound - - Initial states: {FetchMarketDataRound} - - Transition states: - 0. FetchMarketDataRound - - done: 1. - - none: 3. - - no majority: 0. - - round timeout: 0. - 1. TransformMarketDataRound - - done: 2. - - none: 3. - - no majority: 1. - - round timeout: 1. - 2. FinishedMarketFetchRound - 3. FailedMarketFetchRound - - Final states: {FailedMarketFetchRound, FinishedMarketFetchRound} - - Timeouts: - round timeout: 30.0 - """ - - initial_round_cls: AppState = FetchMarketDataRound - initial_states: Set[AppState] = {FetchMarketDataRound} - transition_function: AbciAppTransitionFunction = { - FetchMarketDataRound: { - Event.DONE: TransformMarketDataRound, - Event.NONE: FailedMarketFetchRound, - Event.NO_MAJORITY: FetchMarketDataRound, - Event.ROUND_TIMEOUT: FetchMarketDataRound, - }, - TransformMarketDataRound: { - Event.DONE: FinishedMarketFetchRound, - Event.NONE: FailedMarketFetchRound, - Event.NO_MAJORITY: TransformMarketDataRound, - Event.ROUND_TIMEOUT: TransformMarketDataRound, - }, - FinishedMarketFetchRound: {}, - FailedMarketFetchRound: {}, - } - final_states: Set[AppState] = {FinishedMarketFetchRound, FailedMarketFetchRound} - event_to_timeout: EventToTimeout = { - Event.ROUND_TIMEOUT: 30.0, - } - cross_period_persisted_keys: FrozenSet[str] = frozenset() - db_pre_conditions: Dict[AppState, Set[str]] = { - FetchMarketDataRound: set(), - } - db_post_conditions: Dict[AppState, Set[str]] = { - FinishedMarketFetchRound: {get_name(SynchronizedData.data_hash)}, - FailedMarketFetchRound: set(), - } diff --git a/packages/valory/skills/market_data_fetcher_abci/skill.yaml b/packages/valory/skills/market_data_fetcher_abci/skill.yaml deleted file mode 100644 index fb25ffd..0000000 --- a/packages/valory/skills/market_data_fetcher_abci/skill.yaml +++ /dev/null @@ -1,165 +0,0 @@ -name: market_data_fetcher_abci -author: valory -version: 0.1.0 -type: skill -description: The scaffold skill is a scaffold for your own skill implementation. -license: Apache-2.0 -aea_version: '>=1.0.0, <2.0.0' -fingerprint: - __init__.py: bafybeih2dsi3e5ax36ooryzjkeoprkeqif3sguc4h63ldx6clkhmdsztka - behaviours.py: bafybeidbor36h5a364j2yal4idziy7u5fsofkivcizzasepliz7pxa3pki - dialogues.py: bafybeihqyvgfzkno2kch5jf7qefrwgknhraz345svglrc6bfgtmuo7gbmi - fsm_specification.yaml: bafybeib6ucxlubtfscg7vris2ia2f7iwlpzxte2bhqcvdluoge4xl2paba - handlers.py: bafybeia3jv2qaq7s6ao6b2xoyvqb4wpzc2zhu75cd26xrqjq3puqxesefy - models.py: bafybeieqo2y4yt34wrvdirmbdhxdeggtsfyc3dvwgq4thqqizbtndyzfcu - payloads.py: bafybeiaq5cqqinf34cxfn6lgefspbyhd3bcfcphqx6q2czpkall55db3vu - rounds.py: bafybeibf2cwrf6phrdnwlrfezy2erl4ux56uumc25jarqpwzhtyemtlhuu -fingerprint_ignore_patterns: [] -connections: -- eightballer/dcxt:0.1.0:bafybeihes2id6qmqqybvdqfpchu6h3i5mpnhq6lztfxp52twd22iwqxacq -contracts: [] -protocols: -- eightballer/tickers:0.1.0:bafybeicjbpa24tla2enenmlzipqhu6grutqso74q6y7is2cpk7acub3bca -skills: -- valory/abstract_round_abci:0.1.0:bafybeibovsktd3uxur45nrcomq5shcn46cgxd5idmhxbmjhg32c5abyqim -behaviours: - main: - args: {} - class_name: MarketDataFetcherRoundBehaviour -handlers: - abci: - args: {} - class_name: ABCIHandler - contract_api: - args: {} - class_name: ContractApiHandler - http: - args: {} - class_name: HttpHandler - ipfs: - args: {} - class_name: IpfsHandler - ledger_api: - args: {} - class_name: LedgerApiHandler - signing: - args: {} - class_name: SigningHandler - tendermint: - args: {} - class_name: TendermintHandler - tickers: - args: {} - class_name: DcxtTickersHandler -models: - abci_dialogues: - args: {} - class_name: AbciDialogues - benchmark_tool: - args: - log_dir: /logs - class_name: BenchmarkTool - contract_api_dialogues: - args: {} - class_name: ContractApiDialogues - http_dialogues: - args: {} - class_name: HttpDialogues - ipfs_dialogues: - args: {} - class_name: IpfsDialogues - ledger_api_dialogues: - args: {} - class_name: LedgerApiDialogues - params: - args: - cleanup_history_depth: 1 - cleanup_history_depth_current: null - drand_public_key: 868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31 - finalize_timeout: 60.0 - genesis_config: - chain_id: chain-c4daS1 - consensus_params: - block: - max_bytes: '22020096' - max_gas: '-1' - time_iota_ms: '1000' - evidence: - max_age_duration: '172800000000000' - max_age_num_blocks: '100000' - max_bytes: '1048576' - validator: - pub_key_types: - - ed25519 - version: {} - genesis_time: '2022-05-20T16:00:21.735122717Z' - voting_power: '10' - history_check_timeout: 1205 - ipfs_domain_name: null - keeper_allowed_retries: 3 - keeper_timeout: 30.0 - max_attempts: 10 - max_healthcheck: 120 - on_chain_service_id: null - request_retry_delay: 1.0 - request_timeout: 10.0 - reset_pause_duration: 10 - reset_tendermint_after: 2 - retry_attempts: 400 - retry_timeout: 3 - round_timeout_seconds: 30.0 - service_id: market_data_fetcher - service_registry_address: null - setup: - all_participants: - - '0x0000000000000000000000000000000000000000' - consensus_threshold: null - safe_contract_address: '0x0000000000000000000000000000000000000000' - share_tm_config_on_startup: false - sleep_time: 1 - tendermint_check_sleep_delay: 3 - tendermint_com_url: http://localhost:8080 - tendermint_max_retries: 5 - tendermint_p2p_url: localhost:26656 - tendermint_url: http://localhost:26657 - tx_timeout: 10.0 - validate_timeout: 1205 - token_symbol_whitelist: [] - ledger_ids: - - ethereum - exchange_ids: - ethereum: [] - optimism: - - balancer - class_name: Params - coingecko: - args: - endpoint: https://api.coingecko.com/api/v3/coins/{token_id}/market_chart?vs_currency=usd&days=1 - api_key: null - prices_field: prices - volumes_field: total_volumes - requests_per_minute: 5 - credits: 10000 - rate_limited_code: 429 - class_name: Coingecko - requests: - args: {} - class_name: Requests - signing_dialogues: - args: {} - class_name: SigningDialogues - state: - args: {} - class_name: SharedState - tendermint_dialogues: - args: {} - class_name: TendermintDialogues - tickers_dialogues: - args: {} - class_name: TickersDialogues -dependencies: - pandas: - version: '>=1.3.0' - PyYAML: - version: '>=5.4.1' -is_abstract: true diff --git a/packages/valory/skills/portfolio_tracker_abci/README.md b/packages/valory/skills/portfolio_tracker_abci/README.md deleted file mode 100644 index d581827..0000000 --- a/packages/valory/skills/portfolio_tracker_abci/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Portfolio Tracker abci - -## Description - -This module contains an ABCI skill responsible for tracking the portfolio of the service, -and ensuring that there is enough balance (based on configurable input thresholds) on the agent and the multisig. diff --git a/packages/valory/skills/portfolio_tracker_abci/__init__.py b/packages/valory/skills/portfolio_tracker_abci/__init__.py deleted file mode 100644 index 28b6422..0000000 --- a/packages/valory/skills/portfolio_tracker_abci/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains an ABCI skill responsible for tracking the portfolio of the service.""" - -from aea.configurations.base import PublicId - - -PUBLIC_ID = PublicId.from_str("valory/portfolio_tracker_abci:0.1.0") diff --git a/packages/valory/skills/portfolio_tracker_abci/behaviours.py b/packages/valory/skills/portfolio_tracker_abci/behaviours.py deleted file mode 100644 index e7ffd18..0000000 --- a/packages/valory/skills/portfolio_tracker_abci/behaviours.py +++ /dev/null @@ -1,545 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains round behaviours of `PortfolioTrackerRoundBehaviour`.""" - -import json -from copy import deepcopy -from dataclasses import asdict -from pathlib import Path -from typing import Any, Dict, Generator, Optional, Set, Tuple, Type, cast - -from aea.configurations.constants import _SOLANA_IDENTIFIER - -from packages.eightballer.connections.dcxt.connection import ( - PUBLIC_ID as DCXT_CONNECTION_ID, -) -from packages.eightballer.protocols.balances.message import BalancesMessage -from packages.valory.protocols.ledger_api.custom_types import Kwargs -from packages.valory.protocols.ledger_api.message import LedgerApiMessage -from packages.valory.skills.abstract_round_abci.base import ( - AbstractRound, - LEDGER_API_ADDRESS, -) -from packages.valory.skills.abstract_round_abci.behaviours import ( - AbstractRoundBehaviour, - BaseBehaviour, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - LedgerApiDialogue, - LedgerApiDialogues, -) -from packages.valory.skills.abstract_round_abci.io_.store import SupportedFiletype -from packages.valory.skills.abstract_round_abci.models import ApiSpecs, Requests -from packages.valory.skills.portfolio_tracker_abci.models import ( - GetBalance, - Params, - RPCPayload, - TokenAccounts, -) -from packages.valory.skills.portfolio_tracker_abci.payloads import ( - PortfolioTrackerPayload, -) -from packages.valory.skills.portfolio_tracker_abci.rounds import ( - PortfolioTrackerAbciApp, - PortfolioTrackerRound, - SynchronizedData, -) - - -ledger_to_native_mapping = { - "ethereum": ("ETH", 18), - "xdai": ("XDAI", 18), - "optimism": ("ETH", 18), - "base": ("ETH", 18), -} - -PORTFOLIO_FILENAME = "portfolio.json" -SOL_ADDRESS = "So11111111111111111111111111111111111111112" -BALANCE_METHOD = "getBalance" -TOKEN_ACCOUNTS_METHOD = "getTokenAccountsByOwner" # nosec -TOKEN_ENCODING = "jsonParsed" # nosec -TOKEN_AMOUNT_ACCESS_KEYS = ( - "account", - "data", - "parsed", - "info", - "tokenAmount", - "amount", -) - - -def to_content(content: dict) -> bytes: - """Convert the given content to bytes' payload.""" - return json.dumps(content, sort_keys=True).encode() - - -def safely_get_from_nested_dict( - nested_dict: Dict[str, Any], keys: Tuple[str, ...] -) -> Optional[Any]: - """Get a value safely from a nested dictionary.""" - res = deepcopy(nested_dict) - for key in keys[:-1]: - res = res.get(key, {}) - if not isinstance(res, dict): - return None - - if keys[-1] not in res: - return None - return res[keys[-1]] - - -class PortfolioTrackerBehaviour(BaseBehaviour): - """Behaviour responsible for tracking the portfolio of the service.""" - - matching_round: Type[AbstractRound] = PortfolioTrackerRound - - def __init__(self, **kwargs: Any) -> None: - """Initialize the strategy evaluator behaviour.""" - super().__init__(**kwargs) - self.portfolio: Dict[str, int] = {} - self.portfolio_filepath = Path(self.context.data_dir) / PORTFOLIO_FILENAME - self._performative_to_dialogue_class = { - BalancesMessage.Performative.GET_ALL_BALANCES: self.context.balances_dialogues, - } - - @property - def params(self) -> Params: - """Return the params.""" - return cast(Params, self.context.params) - - @property - def synchronized_data(self) -> SynchronizedData: - """Return the synchronized data.""" - return cast(SynchronizedData, super().synchronized_data) - - @property - def get_balance(self) -> GetBalance: - """Get the `GetBalance` API specs instance.""" - return self.context.get_balance - - @property - def token_accounts(self) -> TokenAccounts: - """Get the `TokenAccounts` API specs instance.""" - return self.context.token_accounts - - @property - def sol_agent_address(self) -> str: - """Get the agent's Solana address.""" - return self.context.agent_addresses[_SOLANA_IDENTIFIER] - - def _handle_response( - self, - api: ApiSpecs, - res: Optional[dict], - ) -> Generator[None, None, Optional[Any]]: - """Handle the response from an API. - - :param api: the `ApiSpecs` instance of the API. - :param res: the response to handle. - :return: the response's result, using the given keys. `None` if response is `None` (has failed). - :yield: None - """ - if res is None: - error = f"Could not get a response from {api.api_id!r} API." - self.context.logger.error(error) - api.increment_retries() - yield from self.sleep(api.retries_info.suggested_sleep_time) - return None - - self.context.logger.info( - f"Retrieved a response from {api.api_id!r} API: {res}." - ) - api.reset_retries() - return res - - def _get_response( - self, - api: ApiSpecs, - dynamic_parameters: Dict[str, str], - content: Optional[dict] = None, - ) -> Generator[None, None, Any]: - """Get the response from an API.""" - specs = api.get_spec() - specs["parameters"].update(dynamic_parameters) - if content is not None: - specs["content"] = to_content(content) - - while not api.is_retries_exceeded(): - res_raw = yield from self.get_http_response(**specs) - res = api.process_response(res_raw) - response = yield from self._handle_response(api, res) - if response is not None: - return response - - error = f"Retries were exceeded for {api.api_id!r} API." - self.context.logger.error(error) - api.reset_retries() - return None - - def get_dcxt_response( - self, - protocol_performative: BalancesMessage.Performative, - **kwargs: Any, - ) -> Generator[None, None, Any]: - """Get a ccxt response.""" - if protocol_performative not in self._performative_to_dialogue_class: - raise ValueError( - f"Unsupported protocol performative {protocol_performative}." - ) - dialogue_class = self._performative_to_dialogue_class[protocol_performative] - - msg, dialogue = dialogue_class.create( - counterparty=str(DCXT_CONNECTION_ID), - performative=protocol_performative, - **kwargs, - ) - msg._sender = str(self.context.skill_id) # pylint: disable=protected-access - response = yield from self._do_request(msg, dialogue) - return response - - def get_solana_native_balance( - self, address: str - ) -> Generator[None, None, Optional[int]]: - """Get the SOL balance of the given address.""" - payload = RPCPayload(BALANCE_METHOD, [address]) - response = yield from self._get_response(self.get_balance, {}, asdict(payload)) - if response is None: - self.context.logger.error("Failed to get SOL balance!") - return response - - def check_solana_balance( - self, multisig: bool - ) -> Generator[None, None, Optional[bool]]: - """Check whether the balance of the multisig or the agent is above the corresponding threshold.""" - if multisig: - address = self.params.squad_vault - theta = self.params.multisig_balance_threshold - which = "vault" - else: - address = self.sol_agent_address - theta = self.params.agent_balance_threshold - which = "agent" - - self.context.logger.info(f"Checking the SOl balance of the {which}...") - balance = yield from self.get_solana_native_balance(address) - if balance is None: - return None - if balance < theta: - self.context.logger.warning( - f"The {which}'s SOL balance is below the specified threshold: {balance} < {theta}" - ) - return False - self.context.logger.info(f"SOL balance of the {which} is sufficient.") - if multisig: - self.portfolio[SOL_ADDRESS] = balance - return True - - def _is_solana_balance_sufficient( - self, ledger_id: str - ) -> Generator[None, None, Optional[bool]]: - """Check whether the balance of the multisig and the agent are above the given thresholds.""" - self.context.logger.info( - f"Checking the SOL balance of the agent and the vault on ledger {ledger_id}..." - ) - agent_balance = yield from self.check_solana_balance(multisig=False) - vault_balance = yield from self.check_solana_balance(multisig=True) - - balances = (agent_balance, vault_balance) - if None in balances: - return None - return all(balances) - - def unexpected_res_format_err(self, res: Any) -> None: - """Error log in case of an unexpected format error.""" - self.context.logger.error( - f"Unexpected response format from {TOKEN_ACCOUNTS_METHOD!r}: {res}" - ) - - def get_token_balance(self, token: str) -> Generator[None, None, Optional[int]]: - """Retrieve the balance of the tokens held in the vault.""" - payload = RPCPayload( - TOKEN_ACCOUNTS_METHOD, - [ - self.params.squad_vault, - {"mint": token}, - {"encoding": TOKEN_ENCODING}, - ], - ) - response = yield from self._get_response( - self.token_accounts, {}, asdict(payload) - ) - if response is None: - return None - - if not isinstance(response, list): - self.unexpected_res_format_err(response) - return None - - if len(response) == 0: - return 0 - - value_content = response.pop(0) - - if not isinstance(value_content, dict): - self.unexpected_res_format_err(response) - return None - - amount = safely_get_from_nested_dict(value_content, TOKEN_AMOUNT_ACCESS_KEYS) - - try: - # typing was warning about int(amount), therefore, we first convert to `str` here - return int(str(amount)) - except ValueError: - self.unexpected_res_format_err(response) - return None - - def _track_solana_portfolio(self, ledger_id: str) -> Generator: - """Track the portfolio of the service.""" - self.context.logger.info( - f"Tracking the portfolio of the service... on ledger {ledger_id}" - ) - should_wait = False - for token in self.params.tracked_tokens: - self.context.logger.info(f"Tracking {token=}...") - - if token == SOL_ADDRESS: - continue - - if should_wait: - yield from self.sleep(self.params.rpc_polling_interval) - should_wait = True - - balance = yield from self.get_solana_token_balance(token) - if balance is None: - self.context.logger.error( - f"Portfolio tracking failed! Could not get the vault's balance for {token=}." - ) - return None - self.portfolio[token] = balance - - def _track_evm_portfolio(self, ledger_id: str) -> Generator: - """Track the portfolio of the service.""" - self.context.logger.info( - f"Tracking the portfolio of the service... on ledger {ledger_id}" - ) - - for exchange in self.params.exchange_ids[ledger_id]: - exchange_id = f"{exchange}" - self.context.logger.info(f"Tracking {exchange_id=} on {ledger_id=}...") - - balances_msg = yield from self.get_dcxt_response( - BalancesMessage.Performative.GET_ALL_BALANCES, # type: ignore - exchange_id=exchange_id, - ledger_id=ledger_id, - address=self.context.params.setup_params["safe_contract_address"], - params={}, - ) - for balance in balances_msg.balances.balances: - self.context.logger.info( - f"Retrieved balance from {exchange_id}: {balance}" - ) - self.portfolio[balance.asset_id] = balance.free - # We also store the balance in the agent's address - # TODO: we implement a mapping of ledger to exchanges, - # so that we can also track the portfolio of the address across different exchanges. - - - def async_act(self) -> Generator: - """Do the act, supporting asynchronous execution.""" - - with self.context.benchmark_tool.measure(self.behaviour_id).local(): - for ledger_id in self.params.ledger_ids: - if ledger_id == _SOLANA_IDENTIFIER: - is_balance_sufficient_func = self._is_solana_balance_sufficient - track_portfolio_func = self._track_solana_portfolio - else: - is_balance_sufficient_func = self._is_evm_balance_sufficient - track_portfolio_func = self._track_evm_portfolio - - if self.synchronized_data.is_balance_sufficient is False: - # wait for some time for the user to take action - sleep_time = self.params.refill_action_timeout - self.context.logger.info( - f"Waiting for a refill. Checking again in {sleep_time} seconds..." - ) - yield from self.sleep(sleep_time) - - is_balance_sufficient = yield from is_balance_sufficient_func(ledger_id) - if is_balance_sufficient is None: - portfolio_hash = None - elif not is_balance_sufficient: - # the value does not matter as the round will transition based on the insufficient balance event - portfolio_hash = "" - else: - yield from track_portfolio_func(ledger_id) - portfolio_hash = yield from self.send_to_ipfs( - str(self.portfolio_filepath), - self.portfolio, - filetype=SupportedFiletype.JSON, - ) - if portfolio_hash is None: - is_balance_sufficient = None - - sender = self.context.agent_address - payload = PortfolioTrackerPayload( - sender, portfolio_hash, is_balance_sufficient - ) - - with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): - yield from self.send_a2a_transaction(payload) - yield from self.wait_until_round_end() - - self.set_done() - - def _is_evm_balance_sufficient( - self, ledger_id: str - ) -> Generator[None, None, Optional[bool]]: - """Check whether the balance of the multisig and the agent are above the given thresholds.""" - self.context.logger.info( - f"Checking the balance of the agent and the vault on ledger {ledger_id}..." - ) - agent_balance = yield from self.check_evm_balance( - multisig=False, ledger_id=ledger_id - ) - vault_balance = yield from self.check_evm_balance( - multisig=True, ledger_id=ledger_id - ) - - balances = (agent_balance, vault_balance) - if None in balances: - return None - return all(balances) - - def check_evm_balance( - self, multisig: bool, ledger_id: str - ) -> Generator[None, None, Optional[bool]]: - """Check whether the balance of the multisig or the agent is above the corresponding threshold.""" - if multisig: - address = self.params.setup_params["safe_contract_address"] - theta = self.params.multisig_balance_threshold - which = "vault" - else: - address = self.context.agent_address - theta = self.params.agent_balance_threshold - which = "agent" - - self.context.logger.info(f"Checking the balance of the {which}...") - balance = yield from self.get_evm_native_balance(address, ledger_id) - # We store in the portfolio the balance of the agent - if balance is None: - return None - self.portfolio[ledger_to_native_mapping[ledger_id][0]] = balance - if balance < theta: - self.context.logger.warning( - f"The {which}'s balance is below the specified threshold: {balance} < {theta}" - ) - return False - self.context.logger.info(f"Balance of the {which} is sufficient.") - return True - - def get_evm_native_balance( - self, address: str, ledger_id: str - ) -> Generator[None, None, Optional[int]]: - """Get the balance of the given address.""" - # We send a request to the ledger to get the balance of the agent. - ledger_api_msg = yield from self.get_ledger_api_response( - address=address, - performative=LedgerApiMessage.Performative.GET_BALANCE, # type: ignore - ledger_id=ledger_id, - ledger_callable="get_balance", - ) - - if ledger_api_msg.performative == LedgerApiMessage.Performative.ERROR: - self.context.logger.error( - f"Failed to get the balance of the agent from ledger {ledger_id}! with error: {ledger_api_msg}" - ) - return None - elif ledger_api_msg.performative == LedgerApiMessage.Performative.BALANCE: - balance = ledger_api_msg.balance - self.context.logger.info( - f"Retrieved balance from ledger {ledger_id}: {balance}" - ) - return balance - else: - self.context.logger.error( - f"Unexpected performative from ledger {ledger_id}: {ledger_api_msg}" - ) - return None - - def get_ledger_api_response( # type: ignore - self, - performative: LedgerApiMessage.Performative, - ledger_callable: str, - ledger_id: str, - address: str, - **kwargs: Any, - ) -> Generator[None, None, LedgerApiMessage]: - """ - Request data from ledger api - - Happy-path full flow of the messages. - - AbstractRoundAbci skill -> (LedgerApiMessage | LedgerApiMessage.Performative) -> Ledger connection - Ledger connection -> (LedgerApiMessage | LedgerApiMessage.Performative) -> AbstractRoundAbci skill - - :param performative: the message performative - :param ledger_callable: the callable to call on the contract - :param kwargs: keyword argument for the contract api request - :return: the contract api response - :yields: the contract api response - """ - ledger_api_dialogues = cast( - LedgerApiDialogues, self.context.ledger_api_dialogues - ) - kwargs = { - "performative": performative, - "counterparty": LEDGER_API_ADDRESS, - "ledger_id": "ethereum", - "callable": ledger_callable, - "address": address, - "kwargs": Kwargs( - { - "chain_id": ledger_id, - } - ), - } - ledger_api_msg, ledger_api_dialogue = ledger_api_dialogues.create(**kwargs) - ledger_api_dialogue = cast( - LedgerApiDialogue, - ledger_api_dialogue, - ) - ledger_api_dialogue.terms = self._get_default_terms() - request_nonce = self._get_request_nonce_from_dialogue(ledger_api_dialogue) - cast(Requests, self.context.requests).request_id_to_callback[ - request_nonce - ] = self.get_callback_request() - self.context.outbox.put_message(message=ledger_api_msg) - response = yield from self.wait_for_message() - return response - - -class PortfolioTrackerRoundBehaviour(AbstractRoundBehaviour): - """PortfolioTrackerRoundBehaviour""" - - initial_behaviour_cls = PortfolioTrackerBehaviour - abci_app_cls = PortfolioTrackerAbciApp - behaviours: Set[Type[BaseBehaviour]] = { - PortfolioTrackerBehaviour, # type: ignore - } diff --git a/packages/valory/skills/portfolio_tracker_abci/dialogues.py b/packages/valory/skills/portfolio_tracker_abci/dialogues.py deleted file mode 100644 index 1739e44..0000000 --- a/packages/valory/skills/portfolio_tracker_abci/dialogues.py +++ /dev/null @@ -1,101 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the dialogues of the FSM app.""" - -from packages.eightballer.protocols.balances.dialogues import ( - BalancesDialogue as BaseBalancesDialogue, -) -from packages.eightballer.protocols.balances.dialogues import ( - BalancesDialogues as BaseBalancesDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - AbciDialogue as BaseAbciDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - AbciDialogues as BaseAbciDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - ContractApiDialogue as BaseContractApiDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - ContractApiDialogues as BaseContractApiDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - HttpDialogue as BaseHttpDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - HttpDialogues as BaseHttpDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - IpfsDialogue as BaseIpfsDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - IpfsDialogues as BaseIpfsDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - LedgerApiDialogue as BaseLedgerApiDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - LedgerApiDialogues as BaseLedgerApiDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - SigningDialogue as BaseSigningDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - SigningDialogues as BaseSigningDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - TendermintDialogue as BaseTendermintDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - TendermintDialogues as BaseTendermintDialogues, -) - - -AbciDialogue = BaseAbciDialogue -AbciDialogues = BaseAbciDialogues - - -HttpDialogue = BaseHttpDialogue -HttpDialogues = BaseHttpDialogues - - -SigningDialogue = BaseSigningDialogue -SigningDialogues = BaseSigningDialogues - - -LedgerApiDialogue = BaseLedgerApiDialogue -LedgerApiDialogues = BaseLedgerApiDialogues - - -ContractApiDialogue = BaseContractApiDialogue -ContractApiDialogues = BaseContractApiDialogues - - -TendermintDialogue = BaseTendermintDialogue -TendermintDialogues = BaseTendermintDialogues - - -IpfsDialogue = BaseIpfsDialogue -IpfsDialogues = BaseIpfsDialogues - - -BaseBalancesDialogue = BaseBalancesDialogue -BaseBalancesDialogues = BaseBalancesDialogues diff --git a/packages/valory/skills/portfolio_tracker_abci/fsm_specification.yaml b/packages/valory/skills/portfolio_tracker_abci/fsm_specification.yaml deleted file mode 100644 index 947811e..0000000 --- a/packages/valory/skills/portfolio_tracker_abci/fsm_specification.yaml +++ /dev/null @@ -1,23 +0,0 @@ -alphabet_in: -- DONE -- FAILED -- INSUFFICIENT_BALANCE -- NO_MAJORITY -- ROUND_TIMEOUT -default_start_state: PortfolioTrackerRound -final_states: -- FailedPortfolioTrackerRound -- FinishedPortfolioTrackerRound -label: PortfolioTrackerAbciApp -start_states: -- PortfolioTrackerRound -states: -- FailedPortfolioTrackerRound -- FinishedPortfolioTrackerRound -- PortfolioTrackerRound -transition_func: - (PortfolioTrackerRound, DONE): FinishedPortfolioTrackerRound - (PortfolioTrackerRound, FAILED): FailedPortfolioTrackerRound - (PortfolioTrackerRound, INSUFFICIENT_BALANCE): PortfolioTrackerRound - (PortfolioTrackerRound, NO_MAJORITY): PortfolioTrackerRound - (PortfolioTrackerRound, ROUND_TIMEOUT): PortfolioTrackerRound diff --git a/packages/valory/skills/portfolio_tracker_abci/handlers.py b/packages/valory/skills/portfolio_tracker_abci/handlers.py deleted file mode 100644 index f2c6dbf..0000000 --- a/packages/valory/skills/portfolio_tracker_abci/handlers.py +++ /dev/null @@ -1,66 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the handlers of the FSM app.""" - -from packages.eightballer.protocols.balances.message import BalancesMessage -from packages.valory.skills.abstract_round_abci.handlers import ( - ABCIRoundHandler as BaseABCIRoundHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import AbstractResponseHandler -from packages.valory.skills.abstract_round_abci.handlers import ( - ContractApiHandler as BaseContractApiHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - HttpHandler as BaseHttpHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - IpfsHandler as BaseIpfsHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - LedgerApiHandler as BaseLedgerApiHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - SigningHandler as BaseSigningHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - TendermintHandler as BaseTendermintHandler, -) - - -class DcxtBalancesHandler(AbstractResponseHandler): - """This class implements a handler for DexBalancesHandler messages.""" - - SUPPORTED_PROTOCOL = BalancesMessage.protocol_id - allowed_response_performatives = frozenset( - { - BalancesMessage.Performative.ALL_BALANCES, - BalancesMessage.Performative.BALANCE, - BalancesMessage.Performative.ERROR, - } - ) - - -ABCIHandler = BaseABCIRoundHandler -HttpHandler = BaseHttpHandler -SigningHandler = BaseSigningHandler -LedgerApiHandler = BaseLedgerApiHandler -ContractApiHandler = BaseContractApiHandler -TendermintHandler = BaseTendermintHandler -IpfsHandler = BaseIpfsHandler diff --git a/packages/valory/skills/portfolio_tracker_abci/models.py b/packages/valory/skills/portfolio_tracker_abci/models.py deleted file mode 100644 index 6ccba44..0000000 --- a/packages/valory/skills/portfolio_tracker_abci/models.py +++ /dev/null @@ -1,100 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the models for the Portfolio Tracker.""" - -from dataclasses import asdict, dataclass -from typing import Any, Dict, Iterable, List, Union - -from packages.valory.skills.abstract_round_abci.models import ApiSpecs, BaseParams -from packages.valory.skills.abstract_round_abci.models import ( - BenchmarkTool as BaseBenchmarkTool, -) -from packages.valory.skills.abstract_round_abci.models import Requests as BaseRequests -from packages.valory.skills.abstract_round_abci.models import ( - SharedState as BaseSharedState, -) -from packages.valory.skills.portfolio_tracker_abci.rounds import PortfolioTrackerAbciApp - - -Requests = BaseRequests -BenchmarkTool = BaseBenchmarkTool - - -class GetBalance(ApiSpecs): - """A model that wraps ApiSpecs for the Solana balance check.""" - - -class TokenAccounts(ApiSpecs): - """A model that wraps ApiSpecs for the Solana tokens' balance check.""" - - -class SharedState(BaseSharedState): - """Keep the current shared state of the skill.""" - - abci_app_cls = PortfolioTrackerAbciApp - - -class Params(BaseParams): - """Parameters.""" - - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Initialize the parameters object.""" - self.agent_balance_threshold: int = self._ensure( - "agent_balance_threshold", kwargs, int - ) - self.multisig_balance_threshold: int = self._ensure( - "multisig_balance_threshold", kwargs, int - ) - self.squad_vault: str = self._ensure("squad_vault", kwargs, str) - self.tracked_tokens: List[str] = self._ensure( - "tracked_tokens", kwargs, List[str] - ) - self.refill_action_timeout: int = self._ensure( - "refill_action_timeout", kwargs, int - ) - self.rpc_polling_interval: int = self._ensure( - "rpc_polling_interval", kwargs, int - ) - # We depend on the same keys across all the models, so we can just use the same keys. - if not getattr(self, "ledger_ids", None): - self.ledger_ids = self._ensure("ledger_ids", kwargs, List[str]) - if not getattr(self, "exchange_ids", None): - self.exchange_ids = self._ensure( - "exchange_ids", kwargs, Dict[str, List[str]] - ) - super().__init__(*args, **kwargs) - - -@dataclass -class RPCPayload: - """An RPC request's payload.""" - - method: str - params: list - id: int = 1 - jsonrpc: str = "2.0" - - def __getitem__(self, attr: str) -> Union[int, str, list]: - """Implemented so we can easily unpack using `**`.""" - return getattr(self, attr) - - def keys(self) -> Iterable[str]: - """Implemented so we can easily unpack using `**`.""" - return asdict(self).keys() diff --git a/packages/valory/skills/portfolio_tracker_abci/payloads.py b/packages/valory/skills/portfolio_tracker_abci/payloads.py deleted file mode 100644 index 2e2b37a..0000000 --- a/packages/valory/skills/portfolio_tracker_abci/payloads.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the transaction payloads of the PortfolioTrackerAbciApp.""" - -from dataclasses import dataclass -from typing import Optional - -from packages.valory.skills.abstract_round_abci.base import BaseTxPayload - - -@dataclass(frozen=True) -class PortfolioTrackerPayload(BaseTxPayload): - """Represent a transaction payload for the portfolio tracker.""" - - portfolio_hash: Optional[str] - is_balance_sufficient: Optional[bool] diff --git a/packages/valory/skills/portfolio_tracker_abci/rounds.py b/packages/valory/skills/portfolio_tracker_abci/rounds.py deleted file mode 100644 index c893f2f..0000000 --- a/packages/valory/skills/portfolio_tracker_abci/rounds.py +++ /dev/null @@ -1,167 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains the rounds of PortfolioTrackerAbciApp.""" - -from enum import Enum -from typing import Dict, Optional, Set, Tuple, cast - -from packages.valory.skills.abstract_round_abci.base import ( - AbciApp, - AbciAppTransitionFunction, - AppState, - BaseSynchronizedData, - CollectSameUntilThresholdRound, - CollectionRound, - DegenerateRound, - DeserializedCollection, - EventToTimeout, - get_name, -) -from packages.valory.skills.portfolio_tracker_abci.payloads import ( - PortfolioTrackerPayload, -) - - -class Event(Enum): - """PortfolioTrackerAbciApp Events""" - - DONE = "done" - INSUFFICIENT_BALANCE = "insufficient_balance" - FAILED = "failed" - NO_MAJORITY = "no_majority" - ROUND_TIMEOUT = "round_timeout" - - -class SynchronizedData(BaseSynchronizedData): - """ - Class to represent the synchronized data. - - This data is replicated by the tendermint application. - """ - - def _get_deserialized(self, key: str) -> DeserializedCollection: - """Strictly get a collection and return it deserialized.""" - serialized = self.db.get_strict(key) - return CollectionRound.deserialize_collection(serialized) - - @property - def portfolio_hash(self) -> Optional[str]: - """Get the hash of the portfolio's data.""" - return self.db.get_strict("portfolio_hash") - - @property - def is_balance_sufficient(self) -> Optional[bool]: - """Get whether the balance is sufficient.""" - return self.db.get("is_balance_sufficient", None) - - @property - def participant_to_portfolio(self) -> DeserializedCollection: - """Get the participants to portfolio tracking.""" - return self._get_deserialized("participant_to_portfolio") - - -class PortfolioTrackerRound(CollectSameUntilThresholdRound): - """PortfolioTrackerRound""" - - payload_class = PortfolioTrackerPayload - synchronized_data_class = SynchronizedData - done_event = Event.DONE - none_event = Event.FAILED - no_majority_event = Event.NO_MAJORITY - selection_key = ( - get_name(SynchronizedData.portfolio_hash), - get_name(SynchronizedData.is_balance_sufficient), - ) - collection_key = get_name(SynchronizedData.participant_to_portfolio) - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: - """Process the end of the block.""" - res = super().end_block() - if res is None: - return None - - synced_data, event = cast(Tuple[SynchronizedData, Enum], res) - if event == self.done_event and not synced_data.is_balance_sufficient: - return synced_data, Event.INSUFFICIENT_BALANCE - return synced_data, event - - -class FinishedPortfolioTrackerRound(DegenerateRound): - """This class represents that the portfolio tracking has finished.""" - - -class FailedPortfolioTrackerRound(DegenerateRound): - """This class represents that the portfolio tracking has failed.""" - - -class PortfolioTrackerAbciApp(AbciApp[Event]): - """PortfolioTrackerAbciApp - - Initial round: PortfolioTrackerRound - - Initial states: {PortfolioTrackerRound} - - Transition states: - 0. PortfolioTrackerRound - - done: 1. - - failed: 2. - - insufficient balance: 0. - - no majority: 0. - - round timeout: 0. - 1. FinishedPortfolioTrackerRound - 2. FailedPortfolioTrackerRound - - Final states: {FailedPortfolioTrackerRound, FinishedPortfolioTrackerRound} - - Timeouts: - round timeout: 30.0 - """ - - initial_round_cls: AppState = PortfolioTrackerRound - initial_states: Set[AppState] = {PortfolioTrackerRound} - transition_function: AbciAppTransitionFunction = { - PortfolioTrackerRound: { - Event.DONE: FinishedPortfolioTrackerRound, - Event.FAILED: FailedPortfolioTrackerRound, - Event.INSUFFICIENT_BALANCE: PortfolioTrackerRound, - Event.NO_MAJORITY: PortfolioTrackerRound, - Event.ROUND_TIMEOUT: PortfolioTrackerRound, - }, - FinishedPortfolioTrackerRound: {}, - FailedPortfolioTrackerRound: {}, - } - final_states: Set[AppState] = { - FinishedPortfolioTrackerRound, - FailedPortfolioTrackerRound, - } - event_to_timeout: EventToTimeout = { - Event.ROUND_TIMEOUT: 30.0, - } - db_pre_conditions: Dict[AppState, Set[str]] = { - PortfolioTrackerRound: set(), - } - db_post_conditions: Dict[AppState, Set[str]] = { - FinishedPortfolioTrackerRound: { - get_name(SynchronizedData.portfolio_hash), - get_name(SynchronizedData.is_balance_sufficient), - get_name(SynchronizedData.participant_to_portfolio), - }, - FailedPortfolioTrackerRound: set(), - } diff --git a/packages/valory/skills/portfolio_tracker_abci/skill.yaml b/packages/valory/skills/portfolio_tracker_abci/skill.yaml deleted file mode 100644 index ae3dbc0..0000000 --- a/packages/valory/skills/portfolio_tracker_abci/skill.yaml +++ /dev/null @@ -1,158 +0,0 @@ -name: portfolio_tracker_abci -author: valory -version: 0.1.0 -type: skill -description: An ABCI skill responsible for tracking the portfolio of the service -license: Apache-2.0 -aea_version: '>=1.0.0, <2.0.0' -fingerprint: - README.md: bafybeif4z5ogzdq474b2k67kwyit5yiwiyoc4n42ucv3j52v2vcpophtnu - __init__.py: bafybeifc2ovpf3gofxafilnv2jgtbn73wrcpwoss6ic7ftvnk2rwboenza - behaviours.py: bafybeiasowd66cebllflpxvahzrc5o65cjaiwvnpnfhlcz7ylvlbdoyltm - dialogues.py: bafybeibhhuam4c4cjhzv2l56pqkqhinpkiiivhr5akbqg765khwha3xauy - fsm_specification.yaml: bafybeicdgdc4qoaco2et6kcommb753xlw3d4wma3x6t57jhc6hdjzfczoy - handlers.py: bafybeidrynxgp6qyxlk2ert6dawcsji4b7dylnszpnb5gufcdc6tyej2ym - models.py: bafybeian54yk62z474g3ujd7k4xwh7duow5i3so2jnn4jhw3f2d7zellaa - payloads.py: bafybeid3sue7scr5dama3krank3gkmsucy6xasvds6jvebkptbpqziurlm - rounds.py: bafybeihcblnqrf3jwtvmwgfrrcss7bs5ihq4e4kpyld2dnc6uibgwkjs2u -fingerprint_ignore_patterns: [] -connections: -- eightballer/dcxt:0.1.0:bafybeihes2id6qmqqybvdqfpchu6h3i5mpnhq6lztfxp52twd22iwqxacq -contracts: [] -protocols: -- eightballer/balances:0.1.0:bafybeiajh5vzhcofdpemm3545t3yh6g4okpwnejvbqchxapo765batiitu -- valory/ledger_api:1.0.0:bafybeihdk6psr4guxmbcrc26jr2cbgzpd5aljkqvpwo64bvaz7tdti2oni -skills: -- valory/abstract_round_abci:0.1.0:bafybeibovsktd3uxur45nrcomq5shcn46cgxd5idmhxbmjhg32c5abyqim -behaviours: - main: - args: {} - class_name: PortfolioTrackerRoundBehaviour -handlers: - abci: - args: {} - class_name: ABCIHandler - contract_api: - args: {} - class_name: ContractApiHandler - http: - args: {} - class_name: HttpHandler - ipfs: - args: {} - class_name: IpfsHandler - ledger_api: - args: {} - class_name: LedgerApiHandler - signing: - args: {} - class_name: SigningHandler - tendermint: - args: {} - class_name: TendermintHandler - balances: - args: {} - class_name: DcxtBalancesHandler -models: - abci_dialogues: - args: {} - class_name: AbciDialogues - benchmark_tool: - args: - log_dir: /logs - class_name: BenchmarkTool - contract_api_dialogues: - args: {} - class_name: ContractApiDialogues - http_dialogues: - args: {} - class_name: HttpDialogues - ipfs_dialogues: - args: {} - class_name: IpfsDialogues - ledger_api_dialogues: - args: {} - class_name: LedgerApiDialogues - params: - args: - cleanup_history_depth: 1 - cleanup_history_depth_current: null - drand_public_key: 868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31 - finalize_timeout: 60.0 - genesis_config: - chain_id: chain-c4daS1 - consensus_params: - block: - max_bytes: '22020096' - max_gas: '-1' - time_iota_ms: '1000' - evidence: - max_age_duration: '172800000000000' - max_age_num_blocks: '100000' - max_bytes: '1048576' - validator: - pub_key_types: - - ed25519 - version: {} - genesis_time: '2022-05-20T16:00:21.735122717Z' - voting_power: '10' - history_check_timeout: 1205 - ipfs_domain_name: null - keeper_allowed_retries: 3 - keeper_timeout: 30.0 - max_attempts: 10 - max_healthcheck: 120 - on_chain_service_id: null - request_retry_delay: 1.0 - request_timeout: 10.0 - reset_pause_duration: 10 - reset_tendermint_after: 2 - retry_attempts: 400 - retry_timeout: 3 - round_timeout_seconds: 30.0 - service_id: portfolio_tracker - service_registry_address: null - setup: - all_participants: - - '0x0000000000000000000000000000000000000000' - consensus_threshold: null - safe_contract_address: '0x0000000000000000000000000000000000000000' - share_tm_config_on_startup: false - sleep_time: 1 - tendermint_check_sleep_delay: 3 - tendermint_com_url: http://localhost:8080 - tendermint_max_retries: 5 - tendermint_p2p_url: localhost:26656 - tendermint_url: http://localhost:26657 - tx_timeout: 10.0 - validate_timeout: 1205 - squad_vault: 39Zh4C687EXLY7CT8gjCxe2hUc3krESjUsqs7A1CKD5E - agent_balance_threshold: 50000000 - multisig_balance_threshold: 1000000000 - tracked_tokens: [] - refill_action_timeout: 10 - rpc_polling_interval: 5 - ledger_ids: - - optimism - exchange_ids: - ethereum: [] - optimism: - - balancer - class_name: Params - requests: - args: {} - class_name: Requests - signing_dialogues: - args: {} - class_name: SigningDialogues - state: - args: {} - class_name: SharedState - tendermint_dialogues: - args: {} - class_name: TendermintDialogues - balances_dialogues: - args: {} - class_name: BalancesDialogues -dependencies: {} -is_abstract: true diff --git a/packages/valory/skills/registration_abci/README.md b/packages/valory/skills/registration_abci/README.md deleted file mode 100644 index a8eeb22..0000000 --- a/packages/valory/skills/registration_abci/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Registration abci - -## Description - -This module contains the ABCI registration skill for an AEA. - -## Behaviours - -* `RegistrationBaseBehaviour` - - Register to the next periods. - -* `RegistrationBehaviour` - - Register to the next periods. - -* `RegistrationStartupBehaviour` - - Register to the next periods. - - -## Handlers - -No Handlers (the skill is purely behavioural). - diff --git a/packages/valory/skills/registration_abci/__init__.py b/packages/valory/skills/registration_abci/__init__.py deleted file mode 100644 index e69b708..0000000 --- a/packages/valory/skills/registration_abci/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the ABCI registration skill for an AEA.""" - -from aea.configurations.base import PublicId - - -PUBLIC_ID = PublicId.from_str("valory/registration_abci:0.1.0") diff --git a/packages/valory/skills/registration_abci/behaviours.py b/packages/valory/skills/registration_abci/behaviours.py deleted file mode 100644 index 4a617b4..0000000 --- a/packages/valory/skills/registration_abci/behaviours.py +++ /dev/null @@ -1,492 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the behaviours for the 'registration_abci' skill.""" - -import datetime -import json -from abc import ABC -from enum import Enum -from typing import Any, Dict, Generator, Optional, Set, Type, cast - -from aea.mail.base import EnvelopeContext - -from packages.valory.connections.p2p_libp2p_client.connection import ( - PUBLIC_ID as P2P_LIBP2P_CLIENT_PUBLIC_ID, -) -from packages.valory.contracts.service_registry.contract import ServiceRegistryContract -from packages.valory.protocols.contract_api import ContractApiMessage -from packages.valory.protocols.http import HttpMessage -from packages.valory.protocols.tendermint import TendermintMessage -from packages.valory.skills.abstract_round_abci.base import ABCIAppInternalError -from packages.valory.skills.abstract_round_abci.behaviour_utils import TimeoutException -from packages.valory.skills.abstract_round_abci.behaviours import ( - AbstractRoundBehaviour, - BaseBehaviour, -) -from packages.valory.skills.abstract_round_abci.utils import parse_tendermint_p2p_url -from packages.valory.skills.registration_abci.dialogues import TendermintDialogues -from packages.valory.skills.registration_abci.models import SharedState -from packages.valory.skills.registration_abci.payloads import RegistrationPayload -from packages.valory.skills.registration_abci.rounds import ( - AgentRegistrationAbciApp, - RegistrationRound, - RegistrationStartupRound, -) - - -NODE = "node_{address}" -WAIT_FOR_BLOCK_TIMEOUT = 60.0 # 1 minute - - -class RegistrationBaseBehaviour(BaseBehaviour, ABC): - """Agent registration to the FSM App.""" - - def async_act(self) -> Generator: - """ - Do the action. - - Steps: - - Build a registration transaction. - - Send the transaction and wait for it to be mined. - - Wait until ABCI application transitions to the next round. - - Go to the next behaviour (set done event). - """ - - with self.context.benchmark_tool.measure(self.behaviour_id).local(): - serialized_db = self.synchronized_data.db.serialize() - payload = RegistrationPayload( - self.context.agent_address, initialisation=serialized_db - ) - - with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): - yield from self.send_a2a_transaction(payload) - yield from self.wait_until_round_end() - - self.set_done() - - -class RegistrationStartupBehaviour(RegistrationBaseBehaviour): - """Agent registration to the FSM App.""" - - ENCODING: str = "utf-8" - matching_round = RegistrationStartupRound - local_tendermint_params: Dict[str, Any] = {} - updated_genesis_data: Dict[str, Any] = {} - collection_complete: bool = False - - @property - def initial_tm_configs(self) -> Dict[str, Dict[str, Any]]: - """A mapping of the other agents' addresses to their initial Tendermint configuration.""" - return self.context.state.initial_tm_configs - - @initial_tm_configs.setter - def initial_tm_configs(self, configs: Dict[str, Dict[str, Any]]) -> None: - """A mapping of the other agents' addresses to their initial Tendermint configuration.""" - self.context.state.initial_tm_configs = configs - - class LogMessages(Enum): - """Log messages used in RegistrationStartupBehaviour""" - - config_sharing = "Sharing Tendermint config on start-up?" - # request personal tendermint configuration - request_personal = "Request validator config from personal Tendermint node" - response_personal = "Response validator config from personal Tendermint node" - failed_personal = "Failed validator config from personal Tendermint node" - # verify deployment on-chain contract - request_verification = "Request service registry contract verification" - response_verification = "Response service registry contract verification" - failed_verification = "Failed service registry contract verification" - # request service info from on-chain contract - request_service_info = "Request on-chain service info" - response_service_info = "Response on-chain service info" - failed_service_info = "Failed on-chain service info" - # request tendermint configuration other agents - request_others = "Request Tendermint config info from other agents" - collection_complete = "Completed collecting Tendermint configuration responses" - # update personal tendermint node config - request_update = "Request update Tendermint node configuration" - response_update = "Response update Tendermint node configuration" - failed_update = "Failed update Tendermint node configuration" - # exceptions - no_contract_address = "Service registry contract address not provided" - no_on_chain_service_id = "On-chain service id not provided" - contract_incorrect = "Service registry contract not correctly deployed" - no_agents_registered = "No agents registered on-chain" - self_not_registered = "This agent is not registered on-chain" - - def __str__(self) -> str: - """For ease of use in formatted string literals""" - return self.value - - @property - def tendermint_parameter_url(self) -> str: - """Tendermint URL for obtaining and updating parameters""" - return f"{self.params.tendermint_com_url}/params" - - def _decode_result( - self, message: HttpMessage, error_log: LogMessages - ) -> Optional[Dict[str, Any]]: - """Decode a http message's body. - - :param message: the http message. - :param error_log: a log to prefix potential errors with. - :return: the message's body, as a dictionary - """ - try: - response = json.loads(message.body.decode()) - except json.JSONDecodeError as error: - self.context.logger.error(f"{error_log}: {error}") - return None - - if not response["status"]: # pragma: no cover - self.context.logger.error(f"{error_log}: {response['error']}") - return None - - return response - - def is_correct_contract( - self, service_registry_address: str - ) -> Generator[None, None, bool]: - """Contract deployment verification.""" - - self.context.logger.info(self.LogMessages.request_verification) - - performative = ContractApiMessage.Performative.GET_STATE - kwargs = dict( - performative=performative, - contract_address=service_registry_address, - contract_id=str(ServiceRegistryContract.contract_id), - contract_callable="verify_contract", - chain_id=self.params.default_chain_id, - ) - contract_api_response = yield from self.get_contract_api_response(**kwargs) # type: ignore - if ( - contract_api_response.performative - is not ContractApiMessage.Performative.STATE - ): - verified = False - log_method = self.context.logger.error - log_message = f"{self.LogMessages.failed_verification} ({kwargs}): {contract_api_response}" - else: - verified = cast(bool, contract_api_response.state.body["verified"]) - log_method = self.context.logger.info - log_message = f"{self.LogMessages.response_verification}: {verified}" - - log_method(log_message) - return verified - - def get_agent_instances( - self, service_registry_address: str, on_chain_service_id: int - ) -> Generator[None, None, Dict[str, Any]]: - """Get service info available on-chain""" - - log_message = self.LogMessages.request_service_info - self.context.logger.info(f"{log_message}") - - performative = ContractApiMessage.Performative.GET_STATE - kwargs = dict( - performative=performative, - contract_address=service_registry_address, - contract_id=str(ServiceRegistryContract.contract_id), - contract_callable="get_agent_instances", - service_id=on_chain_service_id, - chain_id=self.params.default_chain_id, - ) - contract_api_response = yield from self.get_contract_api_response(**kwargs) # type: ignore - if contract_api_response.performative != ContractApiMessage.Performative.STATE: - log_message = self.LogMessages.failed_service_info - self.context.logger.error( - f"{log_message} ({kwargs}): {contract_api_response}" - ) - return {} - - log_message = self.LogMessages.response_service_info - self.context.logger.info(f"{log_message}: {contract_api_response}") - return cast(dict, contract_api_response.state.body) - - def get_addresses(self) -> Generator: # pylint: disable=too-many-return-statements - """Get addresses of agents registered for the service""" - - service_registry_address = self.params.service_registry_address - if service_registry_address is None: - log_message = self.LogMessages.no_contract_address.value - self.context.logger.error(log_message) - return False - - correctly_deployed = yield from self.is_correct_contract( - service_registry_address - ) - if not correctly_deployed: - return False - - on_chain_service_id = self.params.on_chain_service_id - if on_chain_service_id is None: - log_message = self.LogMessages.no_on_chain_service_id.value - self.context.logger.error(log_message) - return False - - service_info = yield from self.get_agent_instances( - service_registry_address, on_chain_service_id - ) - if not service_info: - return False - - registered_addresses = set(service_info["agentInstances"]) - if not registered_addresses: - log_message = self.LogMessages.no_agents_registered.value - self.context.logger.error(f"{log_message}: {service_info}") - return False - - my_address = self.context.agent_address - if my_address not in registered_addresses: - log_message = f"{self.LogMessages.self_not_registered} ({my_address})" - self.context.logger.error(f"{log_message}: {registered_addresses}") - return False - - # put service info in the shared state for p2p message handler - info: Dict[str, Dict[str, str]] = {i: {} for i in registered_addresses} - tm_host, tm_port = parse_tendermint_p2p_url(url=self.params.tendermint_p2p_url) - validator_config = dict( - hostname=tm_host, - p2p_port=tm_port, - address=self.local_tendermint_params["address"], - pub_key=self.local_tendermint_params["pub_key"], - peer_id=self.local_tendermint_params["peer_id"], - ) - info[self.context.agent_address] = validator_config - self.initial_tm_configs = info - log_message = self.LogMessages.response_service_info.value - self.context.logger.info(f"{log_message}: {info}") - return True - - def get_tendermint_configuration(self) -> Generator[None, None, bool]: - """Make HTTP GET request to obtain agent's local Tendermint node parameters""" - - url = self.tendermint_parameter_url - log_message = self.LogMessages.request_personal - self.context.logger.info(f"{log_message}: {url}") - - result = yield from self.get_http_response(method="GET", url=url) - response = self._decode_result(result, self.LogMessages.failed_personal) - if response is None: - return False - - self.local_tendermint_params = response["params"] - log_message = self.LogMessages.response_personal - self.context.logger.info(f"{log_message}: {response}") - return True - - def request_tendermint_info(self) -> Generator[None, None, bool]: - """Request Tendermint info from other agents""" - - still_missing = {k for k, v in self.initial_tm_configs.items() if not v} - { - self.context.agent_address - } - log_message = self.LogMessages.request_others - self.context.logger.info(f"{log_message}: {still_missing}") - - for address in still_missing: - dialogues = cast(TendermintDialogues, self.context.tendermint_dialogues) - performative = TendermintMessage.Performative.GET_GENESIS_INFO - message, _ = dialogues.create( - counterparty=address, performative=performative - ) - message = cast(TendermintMessage, message) - context = EnvelopeContext(connection_id=P2P_LIBP2P_CLIENT_PUBLIC_ID) - self.context.outbox.put_message(message=message, context=context) - # we wait for the messages that were put in the outbox. - yield from self.sleep(self.params.sleep_time) - - if all(self.initial_tm_configs.values()): - log_message = self.LogMessages.collection_complete - self.context.logger.info(f"{log_message}: {self.initial_tm_configs}") - validator_to_agent = { - config["address"]: agent - for agent, config in self.initial_tm_configs.items() - } - self.context.state.setup_slashing(validator_to_agent) - self.collection_complete = True - return self.collection_complete - - def format_genesis_data( - self, - collected_agent_info: Dict[str, Any], - ) -> Dict[str, Any]: - """Format collected agent info for genesis update""" - - validators = [] - for address, validator_config in collected_agent_info.items(): - validator = dict( - hostname=validator_config["hostname"], - p2p_port=validator_config["p2p_port"], - address=validator_config["address"], - pub_key=validator_config["pub_key"], - peer_id=validator_config["peer_id"], - power=self.params.genesis_config.voting_power, - name=NODE.format(address=address[2:]), # skip 0x part - ) - validators.append(validator) - - genesis_data = dict( - validators=validators, - genesis_config=self.params.genesis_config.to_json(), - external_address=self.params.tendermint_p2p_url, - ) - return genesis_data - - def request_update(self) -> Generator[None, None, bool]: - """Make HTTP POST request to update agent's local Tendermint node""" - - url = self.tendermint_parameter_url - genesis_data = self.format_genesis_data(self.initial_tm_configs) - log_message = self.LogMessages.request_update - self.context.logger.info(f"{log_message}: {genesis_data}") - - content = json.dumps(genesis_data).encode(self.ENCODING) - result = yield from self.get_http_response( - method="POST", url=url, content=content - ) - response = self._decode_result(result, self.LogMessages.failed_update) - if response is None: - return False - - log_message = self.LogMessages.response_update - self.context.logger.info(f"{log_message}: {response}") - self.updated_genesis_data.update(genesis_data) - return True - - def wait_for_block(self, timeout: float) -> Generator[None, None, bool]: - """Wait for a block to be received in the specified timeout.""" - # every agent will finish with the reset at a different time - # hence the following will be different for all agents - start_time = datetime.datetime.now() - - def received_block() -> bool: - """Check whether we have received a block after "start_time".""" - try: - shared_state = cast(SharedState, self.context.state) - last_timestamp = shared_state.round_sequence.last_timestamp - if last_timestamp > start_time: - return True - return False - except ABCIAppInternalError: - # this can happen if we haven't received a block yet - return False - - try: - yield from self.wait_for_condition( - condition=received_block, timeout=timeout - ) - # if the `wait_for_condition` finish without an exception, - # it means that the condition has been satisfied on time - return True - except TimeoutException: - # the agent wasn't able to receive blocks in the given amount of time (timeout) - return False - - def async_act(self) -> Generator: # pylint: disable=too-many-return-statements - """ - Do the action. - - Steps: - 1. Collect personal Tendermint configuration - 2. Make Service Registry contract call to retrieve addresses - of the other agents registered on-chain for the service. - 3. Request Tendermint configuration from registered agents. - This is done over the Agent Communication Network using - the p2p_libp2p_client connection. - 4. Update Tendermint configuration via genesis.json with the - information of the other validators (agents). - 5. Restart Tendermint to establish the validator network. - """ - - exchange_config = self.params.share_tm_config_on_startup - log_message = self.LogMessages.config_sharing.value - self.context.logger.info(f"{log_message}: {exchange_config}") - - if not exchange_config: - yield from super().async_act() - return - - self.context.logger.info(f"My address: {self.context.agent_address}") - - # collect personal Tendermint configuration - if not self.local_tendermint_params: - successful = yield from self.get_tendermint_configuration() - if not successful: - yield from self.sleep(self.params.sleep_time) - return - - # if the agent doesn't have it's tm config info set, then make service registry contract call - # to get the rest of the agents, so we can get their tm config info later - info = self.initial_tm_configs.get(self.context.agent_address, None) - if info is None: - successful = yield from self.get_addresses() - if not successful: - yield from self.sleep(self.params.sleep_time) - return - - # collect Tendermint config information from other agents - if not self.collection_complete: - successful = yield from self.request_tendermint_info() - if not successful: - yield from self.sleep(self.params.sleep_time) - return - - # update Tendermint configuration - if not self.updated_genesis_data: - successful = yield from self.request_update() - if not successful: - yield from self.sleep(self.params.sleep_time) - return - - # restart Tendermint with updated configuration - successful = yield from self.reset_tendermint_with_wait(on_startup=True) - if not successful: - yield from self.sleep(self.params.sleep_time) - return - - # the reset has gone through, and at this point tendermint should start - # sending blocks to the agent. However, that might take a while, since - # we rely on 2/3 of the voting power to be active in order for block production - # to begin. In other words, we wait for >=2/3 of the agents to become active. - successful = yield from self.wait_for_block(timeout=WAIT_FOR_BLOCK_TIMEOUT) - if not successful: - yield from self.sleep(self.params.sleep_time) - return - - yield from super().async_act() - - -class RegistrationBehaviour(RegistrationBaseBehaviour): - """Agent registration to the FSM App.""" - - matching_round = RegistrationRound - - -class AgentRegistrationRoundBehaviour(AbstractRoundBehaviour): - """This behaviour manages the consensus stages for the registration.""" - - initial_behaviour_cls = RegistrationStartupBehaviour - abci_app_cls = AgentRegistrationAbciApp - behaviours: Set[Type[BaseBehaviour]] = { - RegistrationBehaviour, # type: ignore - RegistrationStartupBehaviour, # type: ignore - } diff --git a/packages/valory/skills/registration_abci/dialogues.py b/packages/valory/skills/registration_abci/dialogues.py deleted file mode 100644 index fcc14ac..0000000 --- a/packages/valory/skills/registration_abci/dialogues.py +++ /dev/null @@ -1,90 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the classes required for dialogue management.""" -from packages.valory.skills.abstract_round_abci.dialogues import ( - AbciDialogue as BaseAbciDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - AbciDialogues as BaseAbciDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - ContractApiDialogue as BaseContractApiDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - ContractApiDialogues as BaseContractApiDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - HttpDialogue as BaseHttpDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - HttpDialogues as BaseHttpDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - IpfsDialogue as BaseIpfsDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - IpfsDialogues as BaseIpfsDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - LedgerApiDialogue as BaseLedgerApiDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - LedgerApiDialogues as BaseLedgerApiDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - SigningDialogue as BaseSigningDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - SigningDialogues as BaseSigningDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - TendermintDialogue as BaseTendermintDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - TendermintDialogues as BaseTendermintDialogues, -) - - -AbciDialogue = BaseAbciDialogue -AbciDialogues = BaseAbciDialogues - - -HttpDialogue = BaseHttpDialogue -HttpDialogues = BaseHttpDialogues - - -SigningDialogue = BaseSigningDialogue -SigningDialogues = BaseSigningDialogues - - -LedgerApiDialogue = BaseLedgerApiDialogue -LedgerApiDialogues = BaseLedgerApiDialogues - - -ContractApiDialogue = BaseContractApiDialogue -ContractApiDialogues = BaseContractApiDialogues - - -TendermintDialogue = BaseTendermintDialogue -TendermintDialogues = BaseTendermintDialogues - - -IpfsDialogue = BaseIpfsDialogue -IpfsDialogues = BaseIpfsDialogues diff --git a/packages/valory/skills/registration_abci/fsm_specification.yaml b/packages/valory/skills/registration_abci/fsm_specification.yaml deleted file mode 100644 index 478f352..0000000 --- a/packages/valory/skills/registration_abci/fsm_specification.yaml +++ /dev/null @@ -1,18 +0,0 @@ -alphabet_in: -- DONE -- NO_MAJORITY -default_start_state: RegistrationStartupRound -final_states: -- FinishedRegistrationRound -label: AgentRegistrationAbciApp -start_states: -- RegistrationRound -- RegistrationStartupRound -states: -- FinishedRegistrationRound -- RegistrationRound -- RegistrationStartupRound -transition_func: - (RegistrationRound, DONE): FinishedRegistrationRound - (RegistrationRound, NO_MAJORITY): RegistrationRound - (RegistrationStartupRound, DONE): FinishedRegistrationRound diff --git a/packages/valory/skills/registration_abci/handlers.py b/packages/valory/skills/registration_abci/handlers.py deleted file mode 100644 index 3605348..0000000 --- a/packages/valory/skills/registration_abci/handlers.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the handler for the 'registration_abci' skill.""" - -from packages.valory.skills.abstract_round_abci.handlers import ( - ABCIRoundHandler as BaseABCIRoundHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - ContractApiHandler as BaseContractApiHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - HttpHandler as BaseHttpHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - IpfsHandler as BaseIpfsHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - LedgerApiHandler as BaseLedgerApiHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - SigningHandler as BaseSigningHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - TendermintHandler as BaseTendermintHandler, -) - - -ABCIHandler = BaseABCIRoundHandler -HttpHandler = BaseHttpHandler -SigningHandler = BaseSigningHandler -LedgerApiHandler = BaseLedgerApiHandler -ContractApiHandler = BaseContractApiHandler -TendermintHandler = BaseTendermintHandler -IpfsHandler = BaseIpfsHandler diff --git a/packages/valory/skills/registration_abci/models.py b/packages/valory/skills/registration_abci/models.py deleted file mode 100644 index 98c7e53..0000000 --- a/packages/valory/skills/registration_abci/models.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the shared state for the registration abci skill.""" - -from packages.valory.skills.abstract_round_abci.models import BaseParams -from packages.valory.skills.abstract_round_abci.models import ( - BenchmarkTool as BaseBenchmarkTool, -) -from packages.valory.skills.abstract_round_abci.models import Requests as BaseRequests -from packages.valory.skills.abstract_round_abci.models import ( - SharedState as BaseSharedState, -) -from packages.valory.skills.registration_abci.rounds import AgentRegistrationAbciApp - - -class SharedState(BaseSharedState): - """Keep the current shared state of the skill.""" - - abci_app_cls = AgentRegistrationAbciApp - - -Params = BaseParams -Requests = BaseRequests -BenchmarkTool = BaseBenchmarkTool diff --git a/packages/valory/skills/registration_abci/payloads.py b/packages/valory/skills/registration_abci/payloads.py deleted file mode 100644 index bea2fc5..0000000 --- a/packages/valory/skills/registration_abci/payloads.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the transaction payloads for common apps.""" - -from dataclasses import dataclass - -from packages.valory.skills.abstract_round_abci.base import BaseTxPayload - - -@dataclass(frozen=True) -class RegistrationPayload(BaseTxPayload): - """Represent a transaction payload of type 'registration'.""" - - initialisation: str diff --git a/packages/valory/skills/registration_abci/rounds.py b/packages/valory/skills/registration_abci/rounds.py deleted file mode 100644 index a191ddd..0000000 --- a/packages/valory/skills/registration_abci/rounds.py +++ /dev/null @@ -1,178 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the data classes for common apps ABCI application.""" - -from enum import Enum -from typing import Dict, Optional, Set, Tuple - -from packages.valory.skills.abstract_round_abci.base import ( - AbciApp, - AbciAppTransitionFunction, - AppState, - BaseSynchronizedData, - CollectSameUntilAllRound, - CollectSameUntilThresholdRound, - DegenerateRound, - SlashingNotConfiguredError, - get_name, -) -from packages.valory.skills.abstract_round_abci.models import BaseParams -from packages.valory.skills.registration_abci.payloads import RegistrationPayload - - -class Event(Enum): - """Event enumeration for the price estimation demo.""" - - DONE = "done" - ROUND_TIMEOUT = "round_timeout" - NO_MAJORITY = "no_majority" - - -class FinishedRegistrationRound(DegenerateRound): - """A round representing that agent registration has finished""" - - -class RegistrationStartupRound(CollectSameUntilAllRound): - """ - A round in which the agents get registered. - - This round waits until all agents have registered. - """ - - payload_class = RegistrationPayload - synchronized_data_class = BaseSynchronizedData - - @property - def params(self) -> BaseParams: - """Return the params.""" - return self.context.params - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: - """Process the end of the block.""" - if not self.collection_threshold_reached: - return None - - try: - _ = self.context.state.round_sequence.offence_status - # only use slashing if it is configured and the `use_slashing` is set to True - if self.params.use_slashing: - self.context.state.round_sequence.enable_slashing() - except SlashingNotConfiguredError: - self.context.logger.warning("Slashing has not been enabled!") - - self.context.state.round_sequence.sync_db_and_slashing(self.common_payload) - - synchronized_data = self.synchronized_data.update( - participants=tuple(sorted(self.collection)), - synchronized_data_class=self.synchronized_data_class, - ) - - return synchronized_data, Event.DONE - - -class RegistrationRound(CollectSameUntilThresholdRound): - """ - A round in which the agents get registered. - - This rounds waits until the threshold of agents has been reached - and then a further x block confirmations. - """ - - payload_class = RegistrationPayload - required_block_confirmations = 10 - done_event = Event.DONE - synchronized_data_class = BaseSynchronizedData - - # this allows rejoining agents to send payloads - _allow_rejoin_payloads = True - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: - """Process the end of the block.""" - if self.threshold_reached: - self.block_confirmations += 1 - if ( - self.threshold_reached - and self.block_confirmations - > self.required_block_confirmations # we also wait here as it gives more (available) agents time to join - ): - self.synchronized_data.db.sync(self.most_voted_payload) - synchronized_data = self.synchronized_data.update( - participants=tuple(sorted(self.collection)), - synchronized_data_class=self.synchronized_data_class, - ) - return synchronized_data, Event.DONE - if ( - not self.is_majority_possible( - self.collection, self.synchronized_data.nb_participants - ) - and self.block_confirmations > self.required_block_confirmations - ): - return self.synchronized_data, Event.NO_MAJORITY - return None - - -class AgentRegistrationAbciApp(AbciApp[Event]): - """AgentRegistrationAbciApp - - Initial round: RegistrationStartupRound - - Initial states: {RegistrationRound, RegistrationStartupRound} - - Transition states: - 0. RegistrationStartupRound - - done: 2. - 1. RegistrationRound - - done: 2. - - no majority: 1. - 2. FinishedRegistrationRound - - Final states: {FinishedRegistrationRound} - - Timeouts: - round timeout: 30.0 - """ - - initial_round_cls: AppState = RegistrationStartupRound - initial_states: Set[AppState] = {RegistrationStartupRound, RegistrationRound} - transition_function: AbciAppTransitionFunction = { - RegistrationStartupRound: { - Event.DONE: FinishedRegistrationRound, - }, - RegistrationRound: { - Event.DONE: FinishedRegistrationRound, - Event.NO_MAJORITY: RegistrationRound, - }, - FinishedRegistrationRound: {}, - } - final_states: Set[AppState] = { - FinishedRegistrationRound, - } - event_to_timeout: Dict[Event, float] = { - Event.ROUND_TIMEOUT: 30.0, - } - db_pre_conditions: Dict[AppState, Set[str]] = { - RegistrationStartupRound: set(), - RegistrationRound: set(), - } - db_post_conditions: Dict[AppState, Set[str]] = { - FinishedRegistrationRound: { - get_name(BaseSynchronizedData.participants), - }, - } diff --git a/packages/valory/skills/registration_abci/skill.yaml b/packages/valory/skills/registration_abci/skill.yaml deleted file mode 100644 index 02c579a..0000000 --- a/packages/valory/skills/registration_abci/skill.yaml +++ /dev/null @@ -1,151 +0,0 @@ -name: registration_abci -author: valory -version: 0.1.0 -type: skill -description: ABCI application for common apps. -license: Apache-2.0 -aea_version: '>=1.0.0, <2.0.0' -fingerprint: - README.md: bafybeieztbubb6yn5umyt5ulknvb2xxppz5ecxaosxqsaejnrcrrwfu2ji - __init__.py: bafybeigqj2uodavhrygpqn6iah3ljp53z54c5fxyh5ykgkxuhh5lof6pda - behaviours.py: bafybeihal7ku3mvwfbcdm3twzuktnet2io3inshsrpnoee5d5o6mbavg5q - dialogues.py: bafybeicm4bqedlyytfo4icqqbyolo36j2hk7pqh32d3zc5yqg75bt4demm - fsm_specification.yaml: bafybeicx5eutgr4lin7mhwr73xhanuzwdmps7pfoy5f2k7gfxmuec4qbyu - handlers.py: bafybeifby6yecei2d7jvxbqrc3tpyemb7xdb4ood2kny5dqja26qnxrf24 - models.py: bafybeifkfjsfkjy2x32cbuoewxujfgpcl3wk3fji6kq27ofr2zcfe7l5oe - payloads.py: bafybeiacrixfazch2a5ydj7jfk2pnvlxwkygqlwzkfmdeldrj4fqgwyyzm - rounds.py: bafybeifch5qouoop77ef6ghsdflzuy7bcgn4upxjuusxalqzbk53vrxj4q - tests/__init__.py: bafybeiab2s4vkmbz5bc4wggcclapdbp65bosv4en5zaazk5dwmldojpqja - tests/test_behaviours.py: bafybeicwlo3y44sf7gzkyzfuzhwqkax4hln3oforbcvy4uitlgleft3cge - tests/test_dialogues.py: bafybeibeqnpzuzgcfb6yz76htslwsbbpenihswbp7j3qdyq42yswjq25l4 - tests/test_handlers.py: bafybeifpnwaktxckbvclklo6flkm5zqs7apmb33ffs4jrmunoykjbl5lni - tests/test_models.py: bafybeiewxl7nio5av2aukql2u7hlhodzdvjjneleba32abr42xeirrycb4 - tests/test_payloads.py: bafybeifik6ek75ughyd4y6t2lchlmjadkzbrz4hsb332k6ul4pwhlo2oga - tests/test_rounds.py: bafybeidk4d3w5csj6ka7mcq3ikjmv2yccbpwxhp27ujvd7huag3zl5vu2m -fingerprint_ignore_patterns: [] -connections: -- valory/p2p_libp2p_client:0.1.0:bafybeid3xg5k2ol5adflqloy75ibgljmol6xsvzvezebsg7oudxeeolz7e -contracts: -- valory/service_registry:0.1.0:bafybeieqgcuxmz4uxvlyb62mfsf33qy4xwa5lrij4vvcmrtcsfkng43oyq -protocols: -- valory/contract_api:1.0.0:bafybeidgu7o5llh26xp3u3ebq3yluull5lupiyeu6iooi2xyymdrgnzq5i -- valory/http:1.0.0:bafybeifugzl63kfdmwrxwphrnrhj7bn6iruxieme3a4ntzejf6kmtuwmae -- valory/tendermint:0.1.0:bafybeig4mi3vmlv5zpbjbfuzcgida6j5f2nhrpedxicmrrfjweqc5r7cra -skills: -- valory/abstract_round_abci:0.1.0:bafybeibovsktd3uxur45nrcomq5shcn46cgxd5idmhxbmjhg32c5abyqim -behaviours: - main: - args: {} - class_name: AgentRegistrationRoundBehaviour -handlers: - abci: - args: {} - class_name: ABCIHandler - contract_api: - args: {} - class_name: ContractApiHandler - http: - args: {} - class_name: HttpHandler - ipfs: - args: {} - class_name: IpfsHandler - ledger_api: - args: {} - class_name: LedgerApiHandler - signing: - args: {} - class_name: SigningHandler - tendermint: - args: {} - class_name: TendermintHandler -models: - abci_dialogues: - args: {} - class_name: AbciDialogues - benchmark_tool: - args: - log_dir: /logs - class_name: BenchmarkTool - contract_api_dialogues: - args: {} - class_name: ContractApiDialogues - http_dialogues: - args: {} - class_name: HttpDialogues - ipfs_dialogues: - args: {} - class_name: IpfsDialogues - ledger_api_dialogues: - args: {} - class_name: LedgerApiDialogues - params: - args: - cleanup_history_depth: 1 - cleanup_history_depth_current: null - drand_public_key: 868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31 - genesis_config: - genesis_time: '2022-05-20T16:00:21.735122717Z' - chain_id: chain-c4daS1 - consensus_params: - block: - max_bytes: '22020096' - max_gas: '-1' - time_iota_ms: '1000' - evidence: - max_age_num_blocks: '100000' - max_age_duration: '172800000000000' - max_bytes: '1048576' - validator: - pub_key_types: - - ed25519 - version: {} - voting_power: '10' - keeper_timeout: 30.0 - light_slash_unit_amount: 5000000000000000 - max_attempts: 10 - max_healthcheck: 120 - on_chain_service_id: null - request_retry_delay: 1.0 - request_timeout: 10.0 - reset_pause_duration: 10 - reset_tendermint_after: 2 - retry_attempts: 400 - retry_timeout: 3 - round_timeout_seconds: 30.0 - serious_slash_unit_amount: 8000000000000000 - service_id: registration - service_registry_address: null - setup: - all_participants: - - '0x0000000000000000000000000000000000000000' - safe_contract_address: '0x0000000000000000000000000000000000000000' - consensus_threshold: null - share_tm_config_on_startup: false - slash_cooldown_hours: 3 - slash_threshold_amount: 10000000000000000 - sleep_time: 1 - tendermint_check_sleep_delay: 3 - tendermint_com_url: http://localhost:8080 - tendermint_max_retries: 5 - tendermint_p2p_url: localhost:26656 - tendermint_url: http://localhost:26657 - tx_timeout: 10.0 - use_slashing: false - use_termination: false - class_name: Params - requests: - args: {} - class_name: Requests - signing_dialogues: - args: {} - class_name: SigningDialogues - state: - args: {} - class_name: SharedState - tendermint_dialogues: - args: {} - class_name: TendermintDialogues -dependencies: {} -is_abstract: true -customs: [] diff --git a/packages/valory/skills/registration_abci/tests/__init__.py b/packages/valory/skills/registration_abci/tests/__init__.py deleted file mode 100644 index e4fb2e5..0000000 --- a/packages/valory/skills/registration_abci/tests/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Tests for valory/registration_abci skill.""" diff --git a/packages/valory/skills/registration_abci/tests/test_behaviours.py b/packages/valory/skills/registration_abci/tests/test_behaviours.py deleted file mode 100644 index 1c19c13..0000000 --- a/packages/valory/skills/registration_abci/tests/test_behaviours.py +++ /dev/null @@ -1,644 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Tests for valory/registration_abci skill's behaviours.""" - -# pylint: skip-file - -import collections -import datetime -import json -import logging -import time -from contextlib import ExitStack, contextmanager -from copy import deepcopy -from pathlib import Path -from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, cast -from unittest import mock -from unittest.mock import MagicMock -from urllib.parse import urlparse - -import pytest -from _pytest.logging import LogCaptureFixture - -from packages.valory.contracts.service_registry.contract import ServiceRegistryContract -from packages.valory.protocols.contract_api.message import ContractApiMessage -from packages.valory.protocols.tendermint.message import TendermintMessage -from packages.valory.skills.abstract_round_abci.base import AbciAppDB -from packages.valory.skills.abstract_round_abci.behaviour_utils import ( - BaseBehaviour, - TimeoutException, - make_degenerate_behaviour, -) -from packages.valory.skills.abstract_round_abci.test_tools.base import ( - FSMBehaviourBaseCase, -) -from packages.valory.skills.registration_abci import PUBLIC_ID -from packages.valory.skills.registration_abci.behaviours import ( - RegistrationBaseBehaviour, - RegistrationBehaviour, - RegistrationStartupBehaviour, -) -from packages.valory.skills.registration_abci.models import SharedState -from packages.valory.skills.registration_abci.rounds import ( - BaseSynchronizedData as RegistrationSynchronizedData, -) -from packages.valory.skills.registration_abci.rounds import ( - Event, - FinishedRegistrationRound, -) - - -PACKAGE_DIR = Path(__file__).parent.parent - - -SERVICE_REGISTRY_ADDRESS = "0xa51c1fc2f0d1a1b8494ed1fe312d7c3a78ed91c0" -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" -CONTRACT_ID = str(ServiceRegistryContract.contract_id) -ON_CHAIN_SERVICE_ID = 42 -DUMMY_ADDRESS = "localhost" -DUMMY_VALIDATOR_CONFIG = { - "hostname": DUMMY_ADDRESS, - "address": "address", - "pub_key": { - "type": "tendermint/PubKeyEd25519", - "value": "7y7ycBMMABj5Onf74ITYtUS3uZ6SsCQKZML87mIX", - }, - "peer_id": "peer_id", - "p2p_port": 80, -} - - -def test_skill_public_id() -> None: - """Test skill module public ID""" - - assert PUBLIC_ID.name == Path(__file__).parents[1].name - assert PUBLIC_ID.author == Path(__file__).parents[3].name - - -def consume(iterator: Iterable) -> None: - """Consume the iterator""" - collections.deque(iterator, maxlen=0) - - -@contextmanager -def as_context(*contexts: Any) -> Generator[None, None, None]: - """Set contexts""" - with ExitStack() as stack: - consume(map(stack.enter_context, contexts)) - yield - - -class RegistrationAbciBaseCase(FSMBehaviourBaseCase): - """Base case for testing RegistrationAbci FSMBehaviour.""" - - path_to_skill = PACKAGE_DIR - - -class BaseRegistrationTestBehaviour(RegistrationAbciBaseCase): - """Base test case to test RegistrationBehaviour.""" - - behaviour_class = RegistrationBaseBehaviour - next_behaviour_class = BaseBehaviour - - @pytest.mark.parametrize( - "setup_data, expected_initialisation", - ( - ({}, '{"db_data": {"0": {}}, "slashing_config": ""}'), - ({"test": []}, '{"db_data": {"0": {}}, "slashing_config": ""}'), - ( - {"test": [], "valid": [1, 2]}, - '{"db_data": {"0": {"valid": [1, 2]}}, "slashing_config": ""}', - ), - ), - ) - def test_registration( - self, setup_data: Dict, expected_initialisation: Optional[str] - ) -> None: - """Test registration.""" - self.fast_forward_to_behaviour( - self.behaviour, - self.behaviour_class.auto_behaviour_id(), - RegistrationSynchronizedData(AbciAppDB(setup_data=setup_data)), - ) - assert isinstance(self.behaviour.current_behaviour, BaseBehaviour) - assert ( - self.behaviour.current_behaviour.behaviour_id - == self.behaviour_class.auto_behaviour_id() - ) - with mock.patch.object( - self.behaviour.current_behaviour, - "send_a2a_transaction", - side_effect=self.behaviour.current_behaviour.send_a2a_transaction, - ): - self.behaviour.act_wrapper() - assert isinstance( - self.behaviour.current_behaviour.send_a2a_transaction, MagicMock - ) - assert ( - self.behaviour.current_behaviour.send_a2a_transaction.call_args[0][ - 0 - ].initialisation - == expected_initialisation - ) - self.mock_a2a_transaction() - - self._test_done_flag_set() - self.end_round(Event.DONE) - assert ( - self.behaviour.current_behaviour.behaviour_id - == self.next_behaviour_class.auto_behaviour_id() - ) - - -class TestRegistrationStartupBehaviour(RegistrationAbciBaseCase): - """Test case to test RegistrationStartupBehaviour.""" - - behaviour_class = RegistrationStartupBehaviour - next_behaviour_class = make_degenerate_behaviour(FinishedRegistrationRound) - - other_agents: List[str] = ["0xAlice", "0xBob", "0xCharlie"] - _time_in_future = datetime.datetime.now() + datetime.timedelta(hours=10) - _time_in_past = datetime.datetime.now() - datetime.timedelta(hours=10) - - def setup(self, **kwargs: Any) -> None: - """Setup""" - super().setup(**kwargs) - self.state.params.__dict__["sleep_time"] = 0.01 - self.state.params.__dict__["share_tm_config_on_startup"] = True - - def teardown(self, **kwargs: Any) -> None: - """Teardown.""" - super().teardown(**kwargs) - self.state.initial_tm_configs = {} - - @property - def agent_instances(self) -> List[str]: - """Agent instance addresses""" - return [*self.other_agents, self.state.context.agent_address] - - @property - def state(self) -> RegistrationStartupBehaviour: - """Current behavioural state""" - return cast(RegistrationStartupBehaviour, self.behaviour.current_behaviour) - - @property - def logger(self) -> str: - """Logger""" - return "aea.test_agent_name.packages.valory.skills.registration_abci" - - # mock patches - @property - def mocked_service_registry_address(self) -> mock._patch_dict: - """Mocked service registry address""" - return mock.patch.dict( - self.state.params.__dict__, - {"service_registry_address": SERVICE_REGISTRY_ADDRESS}, - ) - - @property - def mocked_on_chain_service_id(self) -> mock._patch_dict: - """Mocked on chain service id""" - return mock.patch.dict( - self.state.params.__dict__, {"on_chain_service_id": ON_CHAIN_SERVICE_ID} - ) - - def mocked_wait_for_condition(self, should_timeout: bool) -> mock._patch: - """Mock BaseBehaviour.wait_for_condition""" - - def dummy_wait_for_condition( - condition: Callable[[], bool], timeout: Optional[float] = None - ) -> Generator[None, None, None]: - """A mock implementation of BaseBehaviour.wait_for_condition""" - # call the condition - condition() - if should_timeout: - # raise in case required - raise TimeoutException() - return - yield - - return mock.patch.object( - self.behaviour.current_behaviour, - "wait_for_condition", - side_effect=dummy_wait_for_condition, - ) - - @property - def mocked_yield_from_sleep(self) -> mock._patch: - """Mock yield from sleep""" - return mock.patch.object(self.behaviour.current_behaviour, "sleep") - - # mock contract calls - def mock_is_correct_contract(self, error_response: bool = False) -> None: - """Mock service registry contract call to for contract verification""" - request_kwargs = dict(performative=ContractApiMessage.Performative.GET_STATE) - state = ContractApiMessage.State(ledger_id="ethereum", body={"verified": True}) - performative = ContractApiMessage.Performative.STATE - if error_response: - performative = ContractApiMessage.Performative.ERROR - response_kwargs = dict( - performative=performative, - callable="verify_contract", - state=state, - ) - self.mock_contract_api_request( - contract_id=CONTRACT_ID, - request_kwargs=request_kwargs, - response_kwargs=response_kwargs, - ) - - def mock_get_agent_instances( - self, *agent_instances: str, error_response: bool = False - ) -> None: - """Mock get agent instances""" - request_kwargs = dict(performative=ContractApiMessage.Performative.GET_STATE) - performative = ContractApiMessage.Performative.STATE - if error_response: - performative = ContractApiMessage.Performative.ERROR - body = {"agentInstances": list(agent_instances)} - state = ContractApiMessage.State(ledger_id="ethereum", body=body) - response_kwargs = dict( - performative=performative, - callable="get_agent_instances", - state=state, - ) - self.mock_contract_api_request( - contract_id=CONTRACT_ID, - request_kwargs=request_kwargs, - response_kwargs=response_kwargs, - ) - - # mock Tendermint config request - def mock_tendermint_request( - self, request_kwargs: Dict, response_kwargs: Dict - ) -> None: - """Mock Tendermint request.""" - - actual_tendermint_message = self.get_message_from_outbox() - assert actual_tendermint_message is not None, "No message in outbox." - has_attributes, error_str = self.message_has_attributes( - actual_message=actual_tendermint_message, - message_type=TendermintMessage, - performative=TendermintMessage.Performative.GET_GENESIS_INFO, - sender=self.state.context.agent_address, - to=actual_tendermint_message.to, - **request_kwargs, - ) - assert has_attributes, error_str - incoming_message = self.build_incoming_message( - message_type=TendermintMessage, - dialogue_reference=( - actual_tendermint_message.dialogue_reference[0], - "stub", - ), - performative=TendermintMessage.Performative.GENESIS_INFO, - target=actual_tendermint_message.message_id, - message_id=-1, - to=self.state.context.agent_address, - sender=actual_tendermint_message.to, - **response_kwargs, - ) - self.tendermint_handler.handle(cast(TendermintMessage, incoming_message)) - - def mock_get_tendermint_info(self, *addresses: str) -> None: - """Mock get Tendermint info""" - for i in addresses: - request_kwargs: Dict = dict() - config = deepcopy(DUMMY_VALIDATOR_CONFIG) - config["address"] = str(config["address"]) + i - info = json.dumps(config) - response_kwargs = dict(info=info) - self.mock_tendermint_request(request_kwargs, response_kwargs) - # give room to the behaviour to finish sleeping - # using the same sleep time here and in the behaviour - # can lead to problems when the sleep here is finished - # before the one in the behaviour - time.sleep(self.state.params.sleep_time * 2) - self.behaviour.act_wrapper() - - # mock HTTP requests - def mock_get_local_tendermint_params(self, valid_response: bool = True) -> None: - """Mock Tendermint get local params""" - url = self.state.tendermint_parameter_url - request_kwargs = dict(method="GET", url=url) - body = b"" - if valid_response: - params = dict(params=DUMMY_VALIDATOR_CONFIG, status=True, error=None) - body = json.dumps(params).encode(self.state.ENCODING) - response_kwargs = dict(status_code=200, body=body) - self.mock_http_request(request_kwargs, response_kwargs) - - def mock_tendermint_update(self, valid_response: bool = True) -> None: - """Mock Tendermint update""" - - validator_configs = self.state.format_genesis_data( - self.state.initial_tm_configs - ) - body = json.dumps(validator_configs).encode(self.state.ENCODING) - url = self.state.tendermint_parameter_url - request_kwargs = dict(method="POST", url=url, body=body) - body = ( - json.dumps({"status": True, "error": None}).encode(self.state.ENCODING) - if valid_response - else b"" - ) - response_kwargs = dict(status_code=200, body=body) - self.mock_http_request(request_kwargs, response_kwargs) - - def set_last_timestamp(self, last_timestamp: Optional[datetime.datetime]) -> None: - """Set last timestamp""" - if last_timestamp is not None: - state = cast(SharedState, self._skill.skill_context.state) - state.round_sequence.blockchain._blocks.append( - MagicMock(timestamp=last_timestamp) - ) - - @staticmethod - def dummy_reset_tendermint_with_wait_wrapper( - valid_response: bool, - ) -> Callable[[], Generator[None, None, Optional[bool]]]: - """Wrapper for a Dummy `reset_tendermint_with_wait` method.""" - - def dummy_reset_tendermint_with_wait( - **_: bool, - ) -> Generator[None, None, Optional[bool]]: - """Dummy `reset_tendermint_with_wait` method.""" - yield - return valid_response - - return dummy_reset_tendermint_with_wait - - # tests - def test_init(self) -> None: - """Empty init""" - assert self.state.initial_tm_configs == {} - assert self.state.local_tendermint_params == {} - assert self.state.updated_genesis_data == {} - - def test_no_contract_address(self, caplog: LogCaptureFixture) -> None: - """Test service registry contract address not provided""" - with as_context( - caplog.at_level(logging.INFO, logger=self.logger), - self.mocked_yield_from_sleep, - ): - self.behaviour.act_wrapper() - self.mock_get_local_tendermint_params() - log_message = self.state.LogMessages.no_contract_address - assert log_message.value in caplog.text - - @pytest.mark.parametrize("valid_response", [True, False]) - def test_request_personal( - self, valid_response: bool, caplog: LogCaptureFixture - ) -> None: - """Test get tendermint configuration""" - - failed_message = self.state.LogMessages.failed_personal - response_message = self.state.LogMessages.response_personal - log_message = [failed_message, response_message][valid_response] - with as_context( - caplog.at_level(logging.INFO, logger=self.logger), - self.mocked_service_registry_address, - self.mocked_yield_from_sleep, - ): - self.behaviour.act_wrapper() - self.mock_get_local_tendermint_params(valid_response=valid_response) - assert log_message.value in caplog.text - - def test_failed_verification(self, caplog: LogCaptureFixture) -> None: - """Test service registry contract not correctly deployed""" - - with as_context( - caplog.at_level(logging.INFO, logger=self.logger), - self.mocked_service_registry_address, - ): - self.behaviour.act_wrapper() - self.mock_get_local_tendermint_params() - self.mock_is_correct_contract(error_response=True) - log_message = self.state.LogMessages.failed_verification - assert log_message.value in caplog.text - - def test_on_chain_service_id_not_set(self, caplog: LogCaptureFixture) -> None: - """Test `get_addresses` when `on_chain_service_id` is `None`.""" - - with as_context( - caplog.at_level(logging.INFO, logger=self.logger), - self.mocked_service_registry_address, - ): - self.behaviour.act_wrapper() - self.mock_get_local_tendermint_params() - self.mock_is_correct_contract() - log_message = self.state.LogMessages.no_on_chain_service_id - assert log_message.value in caplog.text - - def test_failed_service_info(self, caplog: LogCaptureFixture) -> None: - """Test get service info failure""" - - with as_context( - caplog.at_level(logging.INFO, logger=self.logger), - self.mocked_service_registry_address, - self.mocked_on_chain_service_id, - ): - self.behaviour.act_wrapper() - self.mock_get_local_tendermint_params() - self.mock_is_correct_contract() - self.mock_get_agent_instances(error_response=True) - log_message = self.state.LogMessages.failed_service_info - assert log_message.value in caplog.text - - def test_no_agents_registered(self, caplog: LogCaptureFixture) -> None: - """Test no agent instances registered""" - - with as_context( - caplog.at_level(logging.INFO, logger=self.logger), - self.mocked_service_registry_address, - self.mocked_on_chain_service_id, - ): - self.behaviour.act_wrapper() - self.mock_get_local_tendermint_params() - self.mock_is_correct_contract() - self.mock_get_agent_instances() - log_message = self.state.LogMessages.no_agents_registered - assert log_message.value in caplog.text - - def test_self_not_registered(self, caplog: LogCaptureFixture) -> None: - """Test node operator agent not registered""" - - with as_context( - caplog.at_level(logging.INFO, logger=self.logger), - self.mocked_service_registry_address, - self.mocked_on_chain_service_id, - ): - self.behaviour.act_wrapper() - self.mock_get_local_tendermint_params() - self.mock_is_correct_contract() - self.mock_get_agent_instances(*self.other_agents) - log_message = self.state.LogMessages.self_not_registered - assert log_message.value in caplog.text - - def test_response_service_info(self, caplog: LogCaptureFixture) -> None: - """Test registered addresses retrieved""" - - with as_context( - caplog.at_level(logging.INFO, logger=self.logger), - self.mocked_service_registry_address, - self.mocked_on_chain_service_id, - ): - self.behaviour.act_wrapper() - self.mock_get_local_tendermint_params() - self.mock_is_correct_contract() - self.mock_get_agent_instances(*self.agent_instances) - - assert set(self.state.initial_tm_configs) == set(self.agent_instances) - my_info = self.state.initial_tm_configs[self.state.context.agent_address] - assert ( - my_info["hostname"] - == urlparse(self.state.context.params.tendermint_url).hostname - ) - assert not any(map(self.state.initial_tm_configs.get, self.other_agents)) - log_message = self.state.LogMessages.response_service_info - assert log_message.value in caplog.text - - def test_collection_complete(self, caplog: LogCaptureFixture) -> None: - """Test registered addresses retrieved""" - - with as_context( - caplog.at_level(logging.INFO, logger=self.logger), - self.mocked_service_registry_address, - self.mocked_on_chain_service_id, - mock.patch.object( - self._skill.skill_context.state, - "acn_container", - side_effect=lambda: self.agent_instances, - ), - ): - self.behaviour.act_wrapper() - self.mock_get_local_tendermint_params() - self.mock_is_correct_contract() - self.mock_get_agent_instances(*self.agent_instances) - self.mock_get_tendermint_info(*self.other_agents) - - initial_tm_configs = self.state.initial_tm_configs - validator_to_agent = ( - self.state.context.state.round_sequence.validator_to_agent - ) - - assert all(map(initial_tm_configs.get, self.other_agents)) - assert tuple(validator_to_agent.keys()) == tuple( - config["address"] for config in initial_tm_configs.values() - ) - assert tuple(validator_to_agent.values()) == tuple( - initial_tm_configs.keys() - ) - log_message = self.state.LogMessages.collection_complete - assert log_message.value in caplog.text - - @pytest.mark.parametrize("valid_response", [True, False]) - @mock.patch.object(BaseBehaviour, "reset_tendermint_with_wait") - def test_request_update( - self, _: mock.Mock, valid_response: bool, caplog: LogCaptureFixture - ) -> None: - """Test Tendermint config update""" - - self.state.updated_genesis_data = {} - failed_message = self.state.LogMessages.failed_update - response_message = self.state.LogMessages.response_update - log_message = [failed_message, response_message][valid_response] - with as_context( - caplog.at_level(logging.INFO, logger=self.logger), - self.mocked_service_registry_address, - self.mocked_on_chain_service_id, - self.mocked_yield_from_sleep, - mock.patch.object( - self._skill.skill_context.state, - "acn_container", - side_effect=lambda: self.agent_instances, - ), - ): - self.behaviour.act_wrapper() - self.mock_get_local_tendermint_params() - self.mock_is_correct_contract() - self.mock_get_agent_instances(*self.agent_instances) - self.mock_get_tendermint_info(*self.other_agents) - self.mock_tendermint_update(valid_response) - assert log_message.value in caplog.text - - @pytest.mark.parametrize( - "valid_response, last_timestamp, timeout", - [ - (True, _time_in_past, False), - (False, _time_in_past, False), - (True, _time_in_future, False), - (False, _time_in_future, False), - (True, None, False), - (False, None, False), - (True, None, True), - (False, None, True), - ], - ) - def test_request_restart( - self, - valid_response: bool, - last_timestamp: Optional[datetime.datetime], - timeout: bool, - caplog: LogCaptureFixture, - ) -> None: - """Test Tendermint start""" - self.state.updated_genesis_data = {} - self.set_last_timestamp(last_timestamp) - with as_context( - caplog.at_level(logging.INFO, logger=self.logger), - self.mocked_service_registry_address, - self.mocked_on_chain_service_id, - self.mocked_wait_for_condition(timeout), - self.mocked_yield_from_sleep, - mock.patch.object( - self.behaviour.current_behaviour, - "reset_tendermint_with_wait", - side_effect=self.dummy_reset_tendermint_with_wait_wrapper( - valid_response - ), - ), - mock.patch.object( - self._skill.skill_context.state, - "acn_container", - side_effect=lambda: self.agent_instances, - ), - ): - self.behaviour.act_wrapper() - self.mock_get_local_tendermint_params() - self.mock_is_correct_contract() - self.mock_get_agent_instances(*self.agent_instances) - self.mock_get_tendermint_info(*self.other_agents) - self.mock_tendermint_update() - self.behaviour.act_wrapper() - - -class TestRegistrationStartupBehaviourNoConfigShare(BaseRegistrationTestBehaviour): - """Test case to test RegistrationBehaviour.""" - - behaviour_class = RegistrationStartupBehaviour - next_behaviour_class = make_degenerate_behaviour(FinishedRegistrationRound) - - -class TestRegistrationBehaviour(BaseRegistrationTestBehaviour): - """Test case to test RegistrationBehaviour.""" - - behaviour_class = RegistrationBehaviour - next_behaviour_class = make_degenerate_behaviour(FinishedRegistrationRound) diff --git a/packages/valory/skills/registration_abci/tests/test_dialogues.py b/packages/valory/skills/registration_abci/tests/test_dialogues.py deleted file mode 100644 index 55d61a5..0000000 --- a/packages/valory/skills/registration_abci/tests/test_dialogues.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test the dialogues.py module of the skill.""" - -# pylint: skip-file - -import packages.valory.skills.registration_abci.dialogues # noqa - - -def test_import() -> None: - """Test that the 'dialogues.py' Python module can be imported.""" diff --git a/packages/valory/skills/registration_abci/tests/test_handlers.py b/packages/valory/skills/registration_abci/tests/test_handlers.py deleted file mode 100644 index aae35f0..0000000 --- a/packages/valory/skills/registration_abci/tests/test_handlers.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test the dialogues.py module of the skill.""" - -# pylint: skip-file - -import packages.valory.skills.registration_abci.handlers # noqa - - -def test_import() -> None: - """Test that the 'handlers.py' Python module can be imported.""" diff --git a/packages/valory/skills/registration_abci/tests/test_models.py b/packages/valory/skills/registration_abci/tests/test_models.py deleted file mode 100644 index eb6d4d7..0000000 --- a/packages/valory/skills/registration_abci/tests/test_models.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test the models.py module of the skill.""" - -# pylint: skip-file - -from packages.valory.skills.abstract_round_abci.test_tools.base import DummyContext -from packages.valory.skills.registration_abci.models import SharedState - - -class TestSharedState: - """Test SharedState(Model) class.""" - - def test_initialization( - self, - ) -> None: - """Test initialization.""" - SharedState(name="", skill_context=DummyContext()) diff --git a/packages/valory/skills/registration_abci/tests/test_payloads.py b/packages/valory/skills/registration_abci/tests/test_payloads.py deleted file mode 100644 index b9b0a40..0000000 --- a/packages/valory/skills/registration_abci/tests/test_payloads.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test the payloads.py module of the skill.""" - -# pylint: skip-file - -import pytest - -from packages.valory.skills.abstract_round_abci.base import Transaction -from packages.valory.skills.registration_abci.payloads import RegistrationPayload - - -def test_registration_abci_payload() -> None: - """Test `RegistrationPayload`.""" - - payload = RegistrationPayload(sender="sender", initialisation="dummy") - - assert payload.initialisation == "dummy" - assert payload.data == {"initialisation": "dummy"} - assert RegistrationPayload.from_json(payload.json) == payload - - -def test_registration_abci_payload_raises() -> None: - """Test `RegistrationPayload`.""" - payload = RegistrationPayload(sender="sender", initialisation="0" * 10**7) - signature = "signature" - tx = Transaction(payload, signature) - with pytest.raises(ValueError, match="Transaction must be smaller"): - tx.encode() diff --git a/packages/valory/skills/registration_abci/tests/test_rounds.py b/packages/valory/skills/registration_abci/tests/test_rounds.py deleted file mode 100644 index d442800..0000000 --- a/packages/valory/skills/registration_abci/tests/test_rounds.py +++ /dev/null @@ -1,371 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test the rounds.py module of the skill.""" - -import json -from typing import Any, Dict, Optional, cast -from unittest import mock -from unittest.mock import MagicMock, PropertyMock - -import pytest - -from packages.valory.skills.abstract_round_abci.base import AbciAppDB -from packages.valory.skills.abstract_round_abci.base import ( - BaseSynchronizedData as SynchronizedData, -) -from packages.valory.skills.abstract_round_abci.base import ( - CollectSameUntilAllRound, - SlashingNotConfiguredError, -) -from packages.valory.skills.abstract_round_abci.test_tools.rounds import ( - BaseCollectSameUntilAllRoundTest, - BaseCollectSameUntilThresholdRoundTest, -) -from packages.valory.skills.registration_abci.payloads import RegistrationPayload -from packages.valory.skills.registration_abci.rounds import Event as RegistrationEvent -from packages.valory.skills.registration_abci.rounds import ( - RegistrationRound, - RegistrationStartupRound, -) - - -# pylint: skip-file - - -class TestRegistrationStartupRound(BaseCollectSameUntilAllRoundTest): - """Test RegistrationStartupRound.""" - - _synchronized_data_class = SynchronizedData - _event_class = RegistrationEvent - - @pytest.mark.parametrize("slashing_config", ("", json.dumps({"valid": "config"}))) - def test_run_default( - self, - slashing_config: str, - ) -> None: - """Run test.""" - - self.synchronized_data = cast( - SynchronizedData, - self.synchronized_data.update( - safe_contract_address="stub_safe_contract_address", - oracle_contract_address="stub_oracle_contract_address", - ), - ) - - test_round = RegistrationStartupRound( - synchronized_data=self.synchronized_data, - context=MagicMock(), - ) - - self.synchronized_data.slashing_config = slashing_config - - if not slashing_config: - seq = test_round.context.state.round_sequence - type(seq).offence_status = PropertyMock( - side_effect=SlashingNotConfiguredError - ) - - most_voted_payload = self.synchronized_data.db.serialize() - round_payloads = { - participant: RegistrationPayload( - sender=participant, - initialisation=most_voted_payload, - ) - for participant in self.participants - } - - self._run_with_round( - test_round, - round_payloads, - most_voted_payload, - RegistrationEvent.DONE, - ) - - assert all( - ( - self.synchronized_data.all_participants - == frozenset({"agent_2", "agent_0", "agent_1", "agent_3"}), - self.synchronized_data.participants - == frozenset({"agent_2", "agent_0", "agent_1", "agent_3"}), - self.synchronized_data.safe_contract_address - == "stub_safe_contract_address", - self.synchronized_data.db.get("oracle_contract_address") - == "stub_oracle_contract_address", - ) - ) - - test_round.context.state.round_sequence.sync_db_and_slashing.assert_called_once_with( - most_voted_payload - ) - - if slashing_config: - test_round.context.state.round_sequence.enable_slashing.assert_called_once() - else: - test_round.context.state.round_sequence.enable_slashing.assert_not_called() - - def test_run_default_not_finished( - self, - ) -> None: - """Run test.""" - - self.synchronized_data = cast( - SynchronizedData, - self.synchronized_data.update( - safe_contract_address="stub_safe_contract_address", - oracle_contract_address="stub_oracle_contract_address", - ), - ) - test_round = RegistrationStartupRound( - synchronized_data=self.synchronized_data, - context=MagicMock(), - ) - - with mock.patch.object( - CollectSameUntilAllRound, - "collection_threshold_reached", - new_callable=mock.PropertyMock, - ) as threshold_mock: - threshold_mock.return_value = False - self._run_with_round( - test_round, - finished=False, - most_voted_payload="none", - ) - - assert all( - ( - self.synchronized_data.all_participants - == frozenset({"agent_2", "agent_0", "agent_1", "agent_3"}), - self.synchronized_data.participants - == frozenset({"agent_2", "agent_0", "agent_1", "agent_3"}), - self.synchronized_data.safe_contract_address - == "stub_safe_contract_address", - self.synchronized_data.db.get("oracle_contract_address") - == "stub_oracle_contract_address", - ) - ) - - def _run_with_round( - self, - test_round: RegistrationStartupRound, - round_payloads: Optional[Dict[str, RegistrationPayload]] = None, - most_voted_payload: Optional[Any] = None, - expected_event: Optional[RegistrationEvent] = None, - finished: bool = True, - ) -> None: - """Run with given round.""" - - round_payloads = round_payloads or { - p: RegistrationPayload(sender=p, initialisation="none") - for p in self.participants - } - - test_runner = self._test_round( - test_round=test_round, - round_payloads=round_payloads, - synchronized_data_update_fn=( - lambda *x: SynchronizedData( - AbciAppDB( - setup_data=dict(participants=[tuple(self.participants)]), - ) - ) - ), - synchronized_data_attr_checks=[ - lambda _synchronized_data: _synchronized_data.participants - ], - most_voted_payload=most_voted_payload, - exit_event=expected_event, - finished=finished, - ) - - next(test_runner) - next(test_runner) - next(test_runner) - if finished: - next(test_runner) - - -class TestRegistrationRound(BaseCollectSameUntilThresholdRoundTest): - """Test RegistrationRound.""" - - _synchronized_data_class = SynchronizedData - _event_class = RegistrationEvent - - def test_run_default( - self, - ) -> None: - """Run test.""" - self.synchronized_data = cast( - SynchronizedData, - self.synchronized_data.update( - safe_contract_address="stub_safe_contract_address", - oracle_contract_address="stub_oracle_contract_address", - ), - ) - test_round = RegistrationRound( - synchronized_data=self.synchronized_data, - context=MagicMock(), - ) - - payload_data = self.synchronized_data.db.serialize() - - round_payloads = { - participant: RegistrationPayload( - sender=participant, - initialisation=payload_data, - ) - for participant in self.participants - } - - self._run_with_round( - test_round=test_round, - expected_event=RegistrationEvent.DONE, - confirmations=11, - most_voted_payload=payload_data, - round_payloads=round_payloads, - ) - - assert all( - ( - self.synchronized_data.all_participants - == frozenset({"agent_2", "agent_0", "agent_1", "agent_3"}), - self.synchronized_data.participants - == frozenset({"agent_2", "agent_0", "agent_1", "agent_3"}), - self.synchronized_data.safe_contract_address - == "stub_safe_contract_address", - self.synchronized_data.db.get("oracle_contract_address") - == "stub_oracle_contract_address", - ) - ) - - def test_run_default_not_finished( - self, - ) -> None: - """Run test.""" - self.synchronized_data = cast( - SynchronizedData, - self.synchronized_data.update( - safe_contract_address="stub_safe_contract_address", - oracle_contract_address="stub_oracle_contract_address", - ), - ) - test_round = RegistrationRound( - synchronized_data=self.synchronized_data, - context=MagicMock(), - ) - self._run_with_round( - test_round, - confirmations=None, - ) - - assert all( - ( - self.synchronized_data.all_participants - == frozenset({"agent_2", "agent_0", "agent_1", "agent_3"}), - self.synchronized_data.participants - == frozenset({"agent_2", "agent_0", "agent_1", "agent_3"}), - self.synchronized_data.safe_contract_address - == "stub_safe_contract_address", - self.synchronized_data.db.get("oracle_contract_address") - == "stub_oracle_contract_address", - ) - ) - - def _run_with_round( - self, - test_round: RegistrationRound, - round_payloads: Optional[Dict[str, RegistrationPayload]] = None, - most_voted_payload: Optional[Any] = None, - expected_event: Optional[RegistrationEvent] = None, - confirmations: Optional[int] = None, - finished: bool = True, - ) -> None: - """Run with given round.""" - - round_payloads = round_payloads or { - p: RegistrationPayload(sender=p, initialisation="none") - for p in self.participants - } - - test_runner = self._test_round( - test_round=test_round, - round_payloads=round_payloads, - synchronized_data_update_fn=( - lambda *x: SynchronizedData( - AbciAppDB( - setup_data=dict(participants=[tuple(self.participants)]), - ) - ) - ), - synchronized_data_attr_checks=[ - lambda _synchronized_data: _synchronized_data.participants - ], - most_voted_payload=most_voted_payload, - exit_event=expected_event, - ) - - next(test_runner) - if confirmations is None: - assert ( - test_round.block_confirmations - <= test_round.required_block_confirmations - ) - - else: - test_round.block_confirmations = confirmations - test_round = next(test_runner) - prior_confirmations = test_round.block_confirmations - next(test_runner) - assert test_round.block_confirmations == prior_confirmations + 1 - if finished: - next(test_runner) - - def test_no_majority(self) -> None: - """Test the NO_MAJORITY event.""" - self.synchronized_data = cast( - SynchronizedData, - self.synchronized_data.update( - safe_contract_address="stub_safe_contract_address", - oracle_contract_address="stub_oracle_contract_address", - ), - ) - - test_round = RegistrationRound( - synchronized_data=self.synchronized_data, - context=MagicMock(), - ) - - with mock.patch.object(test_round, "is_majority_possible", return_value=False): - with mock.patch.object(test_round, "block_confirmations", 11): - self._test_no_majority_event(test_round) - - assert all( - ( - self.synchronized_data.all_participants - == frozenset({"agent_2", "agent_0", "agent_1", "agent_3"}), - self.synchronized_data.participants - == frozenset({"agent_2", "agent_0", "agent_1", "agent_3"}), - self.synchronized_data.safe_contract_address - == "stub_safe_contract_address", - self.synchronized_data.db.get("oracle_contract_address") - == "stub_oracle_contract_address", - ) - ) diff --git a/packages/valory/skills/reset_pause_abci/README.md b/packages/valory/skills/reset_pause_abci/README.md deleted file mode 100644 index 99ca798..0000000 --- a/packages/valory/skills/reset_pause_abci/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# Reset and pause abci - -## Description - -This module contains the ABCI reset and pause skill for an AEA. It implements an ABCI -application. - -## Behaviours - -* `ResetAndPauseBehaviour` - - Reset state. - -* `ResetPauseABCIConsensusBehaviour` - - This behaviour manages the consensus stages for the reset and pause abci app. - -## Handlers - -* `ResetPauseABCIHandler` -* `HttpHandler` -* `SigningHandler` - diff --git a/packages/valory/skills/reset_pause_abci/__init__.py b/packages/valory/skills/reset_pause_abci/__init__.py deleted file mode 100644 index 850ef39..0000000 --- a/packages/valory/skills/reset_pause_abci/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the Reset & Pause skill for an AEA.""" - -from aea.configurations.base import PublicId - - -PUBLIC_ID = PublicId.from_str("valory/reset_pause_abci:0.1.0") diff --git a/packages/valory/skills/reset_pause_abci/behaviours.py b/packages/valory/skills/reset_pause_abci/behaviours.py deleted file mode 100644 index 9ee30a1..0000000 --- a/packages/valory/skills/reset_pause_abci/behaviours.py +++ /dev/null @@ -1,99 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the behaviours for the 'reset_pause_abci' skill.""" - -from abc import ABC -from typing import Generator, Set, Type, cast - -from packages.valory.skills.abstract_round_abci.base import BaseSynchronizedData -from packages.valory.skills.abstract_round_abci.behaviours import ( - AbstractRoundBehaviour, - BaseBehaviour, -) -from packages.valory.skills.reset_pause_abci.models import Params, SharedState -from packages.valory.skills.reset_pause_abci.payloads import ResetPausePayload -from packages.valory.skills.reset_pause_abci.rounds import ( - ResetAndPauseRound, - ResetPauseAbciApp, -) - - -class ResetAndPauseBaseBehaviour(BaseBehaviour, ABC): - """Reset behaviour.""" - - @property - def synchronized_data(self) -> BaseSynchronizedData: - """Return the synchronized data.""" - return cast( - BaseSynchronizedData, - cast(SharedState, self.context.state).synchronized_data, - ) - - @property - def params(self) -> Params: - """Return the params.""" - return cast(Params, self.context.params) - - -class ResetAndPauseBehaviour(ResetAndPauseBaseBehaviour): - """Reset and pause behaviour.""" - - matching_round = ResetAndPauseRound - - def async_act(self) -> Generator: - """ - Do the action. - - Steps: - - Trivially log the behaviour. - - Sleep for configured interval. - - Build a registration transaction. - - Send the transaction and wait for it to be mined. - - Wait until ABCI application transitions to the next round. - - Go to the next behaviour (set done event). - """ - # + 1 because `period_count` starts from 0 - n_periods_done = self.synchronized_data.period_count + 1 - reset_tm_nodes = n_periods_done % self.params.reset_tendermint_after == 0 - if reset_tm_nodes: - tendermint_reset = yield from self.reset_tendermint_with_wait() - if not tendermint_reset: - return - else: - yield from self.wait_from_last_timestamp(self.params.reset_pause_duration) - self.context.logger.info("Period end.") - self.context.benchmark_tool.save(self.synchronized_data.period_count) - - payload = ResetPausePayload( - self.context.agent_address, self.synchronized_data.period_count - ) - yield from self.send_a2a_transaction(payload, reset_tm_nodes) - yield from self.wait_until_round_end() - self.set_done() - - -class ResetPauseABCIConsensusBehaviour(AbstractRoundBehaviour): - """This behaviour manages the consensus stages for the reset_pause_abci app.""" - - initial_behaviour_cls = ResetAndPauseBehaviour - abci_app_cls = ResetPauseAbciApp - behaviours: Set[Type[BaseBehaviour]] = { - ResetAndPauseBehaviour, # type: ignore - } diff --git a/packages/valory/skills/reset_pause_abci/dialogues.py b/packages/valory/skills/reset_pause_abci/dialogues.py deleted file mode 100644 index e244bde..0000000 --- a/packages/valory/skills/reset_pause_abci/dialogues.py +++ /dev/null @@ -1,91 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the classes required for dialogue management.""" - -from packages.valory.skills.abstract_round_abci.dialogues import ( - AbciDialogue as BaseAbciDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - AbciDialogues as BaseAbciDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - ContractApiDialogue as BaseContractApiDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - ContractApiDialogues as BaseContractApiDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - HttpDialogue as BaseHttpDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - HttpDialogues as BaseHttpDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - IpfsDialogue as BaseIpfsDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - IpfsDialogues as BaseIpfsDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - LedgerApiDialogue as BaseLedgerApiDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - LedgerApiDialogues as BaseLedgerApiDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - SigningDialogue as BaseSigningDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - SigningDialogues as BaseSigningDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - TendermintDialogue as BaseTendermintDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - TendermintDialogues as BaseTendermintDialogues, -) - - -AbciDialogue = BaseAbciDialogue -AbciDialogues = BaseAbciDialogues - - -HttpDialogue = BaseHttpDialogue -HttpDialogues = BaseHttpDialogues - - -SigningDialogue = BaseSigningDialogue -SigningDialogues = BaseSigningDialogues - - -LedgerApiDialogue = BaseLedgerApiDialogue -LedgerApiDialogues = BaseLedgerApiDialogues - - -ContractApiDialogue = BaseContractApiDialogue -ContractApiDialogues = BaseContractApiDialogues - - -TendermintDialogue = BaseTendermintDialogue -TendermintDialogues = BaseTendermintDialogues - - -IpfsDialogue = BaseIpfsDialogue -IpfsDialogues = BaseIpfsDialogues diff --git a/packages/valory/skills/reset_pause_abci/fsm_specification.yaml b/packages/valory/skills/reset_pause_abci/fsm_specification.yaml deleted file mode 100644 index b4882ef..0000000 --- a/packages/valory/skills/reset_pause_abci/fsm_specification.yaml +++ /dev/null @@ -1,19 +0,0 @@ -alphabet_in: -- DONE -- NO_MAJORITY -- RESET_AND_PAUSE_TIMEOUT -default_start_state: ResetAndPauseRound -final_states: -- FinishedResetAndPauseErrorRound -- FinishedResetAndPauseRound -label: ResetPauseAbciApp -start_states: -- ResetAndPauseRound -states: -- FinishedResetAndPauseErrorRound -- FinishedResetAndPauseRound -- ResetAndPauseRound -transition_func: - (ResetAndPauseRound, DONE): FinishedResetAndPauseRound - (ResetAndPauseRound, NO_MAJORITY): FinishedResetAndPauseErrorRound - (ResetAndPauseRound, RESET_AND_PAUSE_TIMEOUT): FinishedResetAndPauseErrorRound diff --git a/packages/valory/skills/reset_pause_abci/handlers.py b/packages/valory/skills/reset_pause_abci/handlers.py deleted file mode 100644 index d6c6335..0000000 --- a/packages/valory/skills/reset_pause_abci/handlers.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the handler for the 'reset_pause_abci' skill.""" - -from packages.valory.skills.abstract_round_abci.handlers import ( - ABCIRoundHandler as BaseABCIRoundHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - ContractApiHandler as BaseContractApiHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - HttpHandler as BaseHttpHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - IpfsHandler as BaseIpfsHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - LedgerApiHandler as BaseLedgerApiHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - SigningHandler as BaseSigningHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - TendermintHandler as BaseTendermintHandler, -) - - -ABCIHandler = BaseABCIRoundHandler -HttpHandler = BaseHttpHandler -SigningHandler = BaseSigningHandler -LedgerApiHandler = BaseLedgerApiHandler -ContractApiHandler = BaseContractApiHandler -TendermintHandler = BaseTendermintHandler -IpfsHandler = BaseIpfsHandler diff --git a/packages/valory/skills/reset_pause_abci/models.py b/packages/valory/skills/reset_pause_abci/models.py deleted file mode 100644 index 89d44fc..0000000 --- a/packages/valory/skills/reset_pause_abci/models.py +++ /dev/null @@ -1,55 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the shared state for the 'reset_pause_abci' application.""" - -from packages.valory.skills.abstract_round_abci.models import BaseParams -from packages.valory.skills.abstract_round_abci.models import ( - BenchmarkTool as BaseBenchmarkTool, -) -from packages.valory.skills.abstract_round_abci.models import Requests as BaseRequests -from packages.valory.skills.abstract_round_abci.models import ( - SharedState as BaseSharedState, -) -from packages.valory.skills.reset_pause_abci.rounds import Event, ResetPauseAbciApp - - -MARGIN = 5 - -Requests = BaseRequests -BenchmarkTool = BaseBenchmarkTool - - -class SharedState(BaseSharedState): - """Keep the current shared state of the skill.""" - - abci_app_cls = ResetPauseAbciApp - - def setup(self) -> None: - """Set up.""" - super().setup() - ResetPauseAbciApp.event_to_timeout[ - Event.ROUND_TIMEOUT - ] = self.context.params.round_timeout_seconds - ResetPauseAbciApp.event_to_timeout[Event.RESET_AND_PAUSE_TIMEOUT] = ( - self.context.params.reset_pause_duration + MARGIN - ) - - -Params = BaseParams diff --git a/packages/valory/skills/reset_pause_abci/payloads.py b/packages/valory/skills/reset_pause_abci/payloads.py deleted file mode 100644 index d5c0058..0000000 --- a/packages/valory/skills/reset_pause_abci/payloads.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the transaction payloads for the reset_pause_abci app.""" - -from dataclasses import dataclass - -from packages.valory.skills.abstract_round_abci.base import BaseTxPayload - - -@dataclass(frozen=True) -class ResetPausePayload(BaseTxPayload): - """Represent a transaction payload of type 'reset'.""" - - period_count: int diff --git a/packages/valory/skills/reset_pause_abci/rounds.py b/packages/valory/skills/reset_pause_abci/rounds.py deleted file mode 100644 index 1fd666a..0000000 --- a/packages/valory/skills/reset_pause_abci/rounds.py +++ /dev/null @@ -1,115 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the data classes for the reset_pause_abci application.""" - -from enum import Enum -from typing import Dict, Optional, Set, Tuple - -from packages.valory.skills.abstract_round_abci.base import ( - AbciApp, - AbciAppTransitionFunction, - AppState, - BaseSynchronizedData, - CollectSameUntilThresholdRound, - DegenerateRound, -) -from packages.valory.skills.reset_pause_abci.payloads import ResetPausePayload - - -class Event(Enum): - """Event enumeration for the reset_pause_abci app.""" - - DONE = "done" - ROUND_TIMEOUT = "round_timeout" - NO_MAJORITY = "no_majority" - RESET_AND_PAUSE_TIMEOUT = "reset_and_pause_timeout" - - -class ResetAndPauseRound(CollectSameUntilThresholdRound): - """A round that represents that consensus is reached (the final round)""" - - payload_class = ResetPausePayload - _allow_rejoin_payloads = True - synchronized_data_class = BaseSynchronizedData - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: - """Process the end of the block.""" - if self.threshold_reached: - return self.synchronized_data.create(), Event.DONE - if not self.is_majority_possible( - self.collection, self.synchronized_data.nb_participants - ): - return self.synchronized_data, Event.NO_MAJORITY - return None - - -class FinishedResetAndPauseRound(DegenerateRound): - """A round that represents reset and pause has finished""" - - -class FinishedResetAndPauseErrorRound(DegenerateRound): - """A round that represents reset and pause has finished with errors""" - - -class ResetPauseAbciApp(AbciApp[Event]): - """ResetPauseAbciApp - - Initial round: ResetAndPauseRound - - Initial states: {ResetAndPauseRound} - - Transition states: - 0. ResetAndPauseRound - - done: 1. - - reset and pause timeout: 2. - - no majority: 2. - 1. FinishedResetAndPauseRound - 2. FinishedResetAndPauseErrorRound - - Final states: {FinishedResetAndPauseErrorRound, FinishedResetAndPauseRound} - - Timeouts: - round timeout: 30.0 - reset and pause timeout: 30.0 - """ - - initial_round_cls: AppState = ResetAndPauseRound - transition_function: AbciAppTransitionFunction = { - ResetAndPauseRound: { - Event.DONE: FinishedResetAndPauseRound, - Event.RESET_AND_PAUSE_TIMEOUT: FinishedResetAndPauseErrorRound, - Event.NO_MAJORITY: FinishedResetAndPauseErrorRound, - }, - FinishedResetAndPauseRound: {}, - FinishedResetAndPauseErrorRound: {}, - } - final_states: Set[AppState] = { - FinishedResetAndPauseRound, - FinishedResetAndPauseErrorRound, - } - event_to_timeout: Dict[Event, float] = { - Event.ROUND_TIMEOUT: 30.0, - Event.RESET_AND_PAUSE_TIMEOUT: 30.0, - } - db_pre_conditions: Dict[AppState, Set[str]] = {ResetAndPauseRound: set()} - db_post_conditions: Dict[AppState, Set[str]] = { - FinishedResetAndPauseRound: set(), - FinishedResetAndPauseErrorRound: set(), - } diff --git a/packages/valory/skills/reset_pause_abci/skill.yaml b/packages/valory/skills/reset_pause_abci/skill.yaml deleted file mode 100644 index e0b97cf..0000000 --- a/packages/valory/skills/reset_pause_abci/skill.yaml +++ /dev/null @@ -1,141 +0,0 @@ -name: reset_pause_abci -author: valory -version: 0.1.0 -type: skill -description: ABCI application for resetting and pausing app executions. -license: Apache-2.0 -aea_version: '>=1.0.0, <2.0.0' -fingerprint: - README.md: bafybeigyx3zutnbq2sqlgeo2hi2vjgpmnlspnkyh4wemjfrqkrpel27bwi - __init__.py: bafybeicx55fcmu5t2lrrs4wqi6bdvsmoq2csfqebyzwy6oh4olmhnvmelu - behaviours.py: bafybeich7tmipn2zsuqsmhtbrmmqys3mpvn3jctx6g3kz2atet2atl3j6q - dialogues.py: bafybeigabhaykiyzbluu4mk6bbrmqhzld2kyp32pg24bvjmzrrb74einwm - fsm_specification.yaml: bafybeietrxvm2odv3si3ecep3by6rftsirzzazxpmeh73yvtsis2mfaali - handlers.py: bafybeie22h45jr2opf2waszr3qt5km2fppcaahalcavhzutgb6pyyywqxq - models.py: bafybeiagj2e73wvzfqti6chbgkxh5tawzdjwqnxlo2bcfa5lyzy6ogzh2u - payloads.py: bafybeihychpsosovpyq7bh6aih2cyjkxr23j7becd5apetrqivvnolzm7i - rounds.py: bafybeifi2gpj2piilxtqcvv6lxhwpnbl7xs3a3trh3wvlv2wihowoon4tm - tests/__init__.py: bafybeiclijinxvycj7agcagt2deuuyh7zxyp7k2s55la6lh3jghzqvfux4 - tests/test_behaviours.py: bafybeigblrmkjci6at74yetaevgw5zszhivpednbok7fby4tqn7zt2vemy - tests/test_dialogues.py: bafybeif7pe7v34cfznzv4htyuevx733ersmk4bqjcgajn2535jmuujdmzm - tests/test_handlers.py: bafybeiggog2k65ijtvqwkvjvmaoo6khwgfkeodddzl6u76gcvvongwjawy - tests/test_payloads.py: bafybeifj343tlaiasebfgahfxehn4oi74omgah3ju2pze2fefoouid2zdq - tests/test_rounds.py: bafybeifz67lfay4pkz5ipblpfpadl4zmd5riajkv6sdsiby22z24gp3cxa -fingerprint_ignore_patterns: [] -connections: [] -contracts: [] -protocols: [] -skills: -- valory/abstract_round_abci:0.1.0:bafybeibovsktd3uxur45nrcomq5shcn46cgxd5idmhxbmjhg32c5abyqim -behaviours: - main: - args: {} - class_name: ResetPauseABCIConsensusBehaviour -handlers: - abci: - args: {} - class_name: ABCIHandler - contract_api: - args: {} - class_name: ContractApiHandler - http: - args: {} - class_name: HttpHandler - ipfs: - args: {} - class_name: IpfsHandler - ledger_api: - args: {} - class_name: LedgerApiHandler - signing: - args: {} - class_name: SigningHandler - tendermint: - args: {} - class_name: TendermintHandler -models: - abci_dialogues: - args: {} - class_name: AbciDialogues - benchmark_tool: - args: - log_dir: /logs - class_name: BenchmarkTool - contract_api_dialogues: - args: {} - class_name: ContractApiDialogues - http_dialogues: - args: {} - class_name: HttpDialogues - ipfs_dialogues: - args: {} - class_name: IpfsDialogues - ledger_api_dialogues: - args: {} - class_name: LedgerApiDialogues - params: - args: - cleanup_history_depth: 1 - cleanup_history_depth_current: null - drand_public_key: 868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31 - genesis_config: - genesis_time: '2022-05-20T16:00:21.735122717Z' - chain_id: chain-c4daS1 - consensus_params: - block: - max_bytes: '22020096' - max_gas: '-1' - time_iota_ms: '1000' - evidence: - max_age_num_blocks: '100000' - max_age_duration: '172800000000000' - max_bytes: '1048576' - validator: - pub_key_types: - - ed25519 - version: {} - voting_power: '10' - keeper_timeout: 30.0 - light_slash_unit_amount: 5000000000000000 - max_attempts: 10 - max_healthcheck: 120 - on_chain_service_id: null - request_retry_delay: 1.0 - request_timeout: 10.0 - reset_pause_duration: 10 - reset_tendermint_after: 2 - retry_attempts: 400 - retry_timeout: 3 - round_timeout_seconds: 30.0 - serious_slash_unit_amount: 8000000000000000 - service_id: reset_pause_abci - service_registry_address: null - setup: {} - share_tm_config_on_startup: false - slash_cooldown_hours: 3 - slash_threshold_amount: 10000000000000000 - sleep_time: 1 - tendermint_check_sleep_delay: 3 - tendermint_com_url: http://localhost:8080 - tendermint_max_retries: 5 - tendermint_p2p_url: localhost:26656 - tendermint_url: http://localhost:26657 - tx_timeout: 10.0 - use_slashing: false - use_termination: false - class_name: Params - requests: - args: {} - class_name: Requests - signing_dialogues: - args: {} - class_name: SigningDialogues - state: - args: {} - class_name: SharedState - tendermint_dialogues: - args: {} - class_name: TendermintDialogues -dependencies: {} -is_abstract: true -customs: [] diff --git a/packages/valory/skills/reset_pause_abci/tests/__init__.py b/packages/valory/skills/reset_pause_abci/tests/__init__.py deleted file mode 100644 index db0918f..0000000 --- a/packages/valory/skills/reset_pause_abci/tests/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Tests for valory/reset_pause_abci skill.""" diff --git a/packages/valory/skills/reset_pause_abci/tests/test_behaviours.py b/packages/valory/skills/reset_pause_abci/tests/test_behaviours.py deleted file mode 100644 index 5435676..0000000 --- a/packages/valory/skills/reset_pause_abci/tests/test_behaviours.py +++ /dev/null @@ -1,154 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Tests for valory/reset_pause_abci skill's behaviours.""" - -# pylint: skip-file - -from pathlib import Path -from typing import Callable, Generator, Optional -from unittest import mock -from unittest.mock import MagicMock - -import pytest - -from packages.valory.skills.abstract_round_abci.base import AbciAppDB -from packages.valory.skills.abstract_round_abci.base import ( - BaseSynchronizedData as ResetSynchronizedSata, -) -from packages.valory.skills.abstract_round_abci.behaviour_utils import ( - make_degenerate_behaviour, -) -from packages.valory.skills.abstract_round_abci.test_tools.base import ( - FSMBehaviourBaseCase, -) -from packages.valory.skills.reset_pause_abci import PUBLIC_ID -from packages.valory.skills.reset_pause_abci.behaviours import ResetAndPauseBehaviour -from packages.valory.skills.reset_pause_abci.rounds import Event as ResetEvent -from packages.valory.skills.reset_pause_abci.rounds import FinishedResetAndPauseRound - - -PACKAGE_DIR = Path(__file__).parent.parent - - -def test_skill_public_id() -> None: - """Test skill module public ID""" - - assert PUBLIC_ID.name == Path(__file__).parents[1].name - assert PUBLIC_ID.author == Path(__file__).parents[3].name - - -class ResetPauseAbciFSMBehaviourBaseCase(FSMBehaviourBaseCase): - """Base case for testing PauseReset FSMBehaviour.""" - - path_to_skill = PACKAGE_DIR - - -def dummy_reset_tendermint_with_wait_wrapper( - reset_successfully: Optional[bool], -) -> Callable[[], Generator[None, None, Optional[bool]]]: - """Wrapper for a Dummy `reset_tendermint_with_wait` method.""" - - def dummy_reset_tendermint_with_wait() -> Generator[None, None, Optional[bool]]: - """Dummy `reset_tendermint_with_wait` method.""" - yield - return reset_successfully - - return dummy_reset_tendermint_with_wait - - -class TestResetAndPauseBehaviour(ResetPauseAbciFSMBehaviourBaseCase): - """Test ResetBehaviour.""" - - behaviour_class = ResetAndPauseBehaviour - next_behaviour_class = make_degenerate_behaviour(FinishedResetAndPauseRound) - - @pytest.mark.parametrize("tendermint_reset_status", (None, True, False)) - def test_reset_behaviour( - self, - tendermint_reset_status: Optional[bool], - ) -> None: - """Test reset behaviour.""" - dummy_participants = [[i for i in range(4)]] - - self.fast_forward_to_behaviour( - behaviour=self.behaviour, - behaviour_id=self.behaviour_class.auto_behaviour_id(), - synchronized_data=ResetSynchronizedSata( - AbciAppDB( - setup_data=dict( - all_participants=dummy_participants, - participants=dummy_participants, - safe_contract_address=[""], - consensus_threshold=[3], - most_voted_estimate=[0.1], - tx_hashes_history=[["68656c6c6f776f726c64"]], - ), - ) - ), - ) - - assert self.behaviour.current_behaviour is not None - assert ( - self.behaviour.current_behaviour.behaviour_id - == self.behaviour_class.auto_behaviour_id() - ) - - with mock.patch.object( - self.behaviour.current_behaviour, - "send_a2a_transaction", - side_effect=self.behaviour.current_behaviour.send_a2a_transaction, - ), mock.patch.object( - self.behaviour.current_behaviour, - "reset_tendermint_with_wait", - side_effect=dummy_reset_tendermint_with_wait_wrapper( - tendermint_reset_status - ), - ) as mock_reset_tendermint_with_wait, mock.patch.object( - self.behaviour.current_behaviour, - "wait_from_last_timestamp", - side_effect=lambda _: (yield), - ): - if tendermint_reset_status is not None: - # Increase the period_count to force the call to reset_tendermint_with_wait() - self.behaviour.current_behaviour.synchronized_data.create() - - self.behaviour.act_wrapper() - self.behaviour.act_wrapper() - - # now if the first reset attempt has been simulated to fail, let's simulate the second attempt to succeed. - if tendermint_reset_status is not None and not tendermint_reset_status: - mock_reset_tendermint_with_wait.side_effect = ( - dummy_reset_tendermint_with_wait_wrapper(True) - ) - self.behaviour.act_wrapper() - # make sure that the behaviour does not send any txs to other agents when Tendermint reset fails - assert isinstance( - self.behaviour.current_behaviour.send_a2a_transaction, MagicMock - ) - self.behaviour.current_behaviour.send_a2a_transaction.assert_not_called() - self.behaviour.act_wrapper() - - self.mock_a2a_transaction() - self._test_done_flag_set() - self.end_round(ResetEvent.DONE) - assert ( - self.behaviour.current_behaviour.behaviour_id - == self.next_behaviour_class.auto_behaviour_id() - ) diff --git a/packages/valory/skills/reset_pause_abci/tests/test_dialogues.py b/packages/valory/skills/reset_pause_abci/tests/test_dialogues.py deleted file mode 100644 index d9b2904..0000000 --- a/packages/valory/skills/reset_pause_abci/tests/test_dialogues.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test the dialogues.py module of the skill.""" - -# pylint: skip-file - -import packages.valory.skills.reset_pause_abci.dialogues # noqa - - -def test_import() -> None: - """Test that the 'dialogues.py' Python module can be imported.""" diff --git a/packages/valory/skills/reset_pause_abci/tests/test_handlers.py b/packages/valory/skills/reset_pause_abci/tests/test_handlers.py deleted file mode 100644 index 347f9e9..0000000 --- a/packages/valory/skills/reset_pause_abci/tests/test_handlers.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test the dialogues.py module of the skill.""" - -# pylint: skip-file - -import packages.valory.skills.reset_pause_abci.handlers # noqa - - -def test_import() -> None: - """Test that the 'handlers.py' Python module can be imported.""" diff --git a/packages/valory/skills/reset_pause_abci/tests/test_payloads.py b/packages/valory/skills/reset_pause_abci/tests/test_payloads.py deleted file mode 100644 index 15b67fa..0000000 --- a/packages/valory/skills/reset_pause_abci/tests/test_payloads.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test the payloads.py module of the skill.""" - -# pylint: skip-file - -from packages.valory.skills.reset_pause_abci.payloads import ResetPausePayload - - -def test_reset_pause_payload() -> None: - """Test `ResetPausePayload`.""" - - payload = ResetPausePayload(sender="sender", period_count=1) - - assert payload.period_count == 1 - assert payload.data == {"period_count": 1} - assert ResetPausePayload.from_json(payload.json) == payload diff --git a/packages/valory/skills/reset_pause_abci/tests/test_rounds.py b/packages/valory/skills/reset_pause_abci/tests/test_rounds.py deleted file mode 100644 index adadb77..0000000 --- a/packages/valory/skills/reset_pause_abci/tests/test_rounds.py +++ /dev/null @@ -1,106 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test the rounds of the skill.""" - -# pylint: skip-file - -import hashlib -import logging # noqa: F401 -from typing import Dict, FrozenSet -from unittest.mock import MagicMock - -from packages.valory.skills.abstract_round_abci.base import ( - BaseSynchronizedData as ResetSynchronizedSata, -) -from packages.valory.skills.abstract_round_abci.test_tools.rounds import ( - BaseCollectSameUntilThresholdRoundTest, -) -from packages.valory.skills.reset_pause_abci.payloads import ResetPausePayload -from packages.valory.skills.reset_pause_abci.rounds import Event as ResetEvent -from packages.valory.skills.reset_pause_abci.rounds import ResetAndPauseRound - - -MAX_PARTICIPANTS: int = 4 -DUMMY_RANDOMNESS = hashlib.sha256("hash".encode() + str(0).encode()).hexdigest() - - -def get_participant_to_period_count( - participants: FrozenSet[str], period_count: int -) -> Dict[str, ResetPausePayload]: - """participant_to_selection""" - return { - participant: ResetPausePayload(sender=participant, period_count=period_count) - for participant in participants - } - - -class TestResetAndPauseRound(BaseCollectSameUntilThresholdRoundTest): - """Test ResetRound.""" - - _synchronized_data_class = ResetSynchronizedSata - _event_class = ResetEvent - - def test_runs( - self, - ) -> None: - """Runs tests.""" - - synchronized_data = self.synchronized_data.update( - most_voted_randomness=DUMMY_RANDOMNESS, consensus_threshold=3 - ) - synchronized_data._db._cross_period_persisted_keys = frozenset( - {"most_voted_randomness"} - ) - test_round = ResetAndPauseRound( - synchronized_data=synchronized_data, - context=MagicMock(), - ) - next_period_count = 1 - self._complete_run( - self._test_round( - test_round=test_round, - round_payloads=get_participant_to_period_count( - self.participants, next_period_count - ), - synchronized_data_update_fn=lambda _synchronized_data, _: _synchronized_data.create(), - synchronized_data_attr_checks=[], # [lambda _synchronized_data: _synchronized_data.participants], - most_voted_payload=next_period_count, - exit_event=self._event_class.DONE, - ) - ) - - def test_accepting_payloads_from(self) -> None: - """Test accepting payloads from""" - - alice, *others = self.participants - participants = list(others) - all_participants = participants + [alice] - - synchronized_data = self.synchronized_data.update( - participants=participants, all_participants=all_participants - ) - - test_round = ResetAndPauseRound( - synchronized_data=synchronized_data, - context=MagicMock(), - ) - - assert test_round.accepting_payloads_from != participants - assert test_round.accepting_payloads_from == frozenset(all_participants) diff --git a/packages/valory/skills/strategy_evaluator_abci/README.md b/packages/valory/skills/strategy_evaluator_abci/README.md deleted file mode 100644 index 5b6617a..0000000 --- a/packages/valory/skills/strategy_evaluator_abci/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# StrategyEvaluator abci - -## Description - -This module contains an ABCI skill responsible for the execution of a trading strategy, -and the preparation of a swapping transaction for a Solana trading AEA. diff --git a/packages/valory/skills/strategy_evaluator_abci/__init__.py b/packages/valory/skills/strategy_evaluator_abci/__init__.py deleted file mode 100644 index f399306..0000000 --- a/packages/valory/skills/strategy_evaluator_abci/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the strategy evaluator skill for the trader.""" - -from aea.configurations.base import PublicId - - -PUBLIC_ID = PublicId.from_str("valory/strategy_evaluator_abci:0.1.0") diff --git a/packages/valory/skills/strategy_evaluator_abci/behaviours/__init__.py b/packages/valory/skills/strategy_evaluator_abci/behaviours/__init__.py deleted file mode 100644 index b05652b..0000000 --- a/packages/valory/skills/strategy_evaluator_abci/behaviours/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains the behaviours for the 'strategy_evaluator_abci' skill.""" diff --git a/packages/valory/skills/strategy_evaluator_abci/behaviours/backtesting.py b/packages/valory/skills/strategy_evaluator_abci/behaviours/backtesting.py deleted file mode 100644 index f9cae76..0000000 --- a/packages/valory/skills/strategy_evaluator_abci/behaviours/backtesting.py +++ /dev/null @@ -1,132 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the behaviour for backtesting the swap(s).""" - -from typing import Any, Dict, Generator, List, Optional, Tuple, cast - -from packages.valory.skills.abstract_round_abci.io_.store import SupportedFiletype -from packages.valory.skills.strategy_evaluator_abci.behaviours.base import ( - CALLABLE_KEY, - STRATEGY_KEY, - StrategyEvaluatorBaseBehaviour, -) -from packages.valory.skills.strategy_evaluator_abci.behaviours.strategy_exec import ( - OUTPUT_MINT, - TRANSFORMED_PRICE_DATA_KEY, -) -from packages.valory.skills.strategy_evaluator_abci.states.backtesting import ( - BacktestRound, -) - - -EVALUATE_CALLABLE_KEY = "evaluate_callable" -ASSET_KEY = "asset" -BACKTEST_RESULT_KEY = "sharpe_ratio" - - -class BacktestBehaviour(StrategyEvaluatorBaseBehaviour): - """A behaviour in which the agents backtest the swap(s).""" - - matching_round = BacktestRound - - def backtest(self, transformed_data: Dict[str, Any], output_mint: str) -> bool: - """Backtest the given token and return whether the sharpe ratio is greater than the threshold.""" - token_data = transformed_data.get(output_mint, None) - if token_data is None: - self.context.logger.error( - f"No data were found in the fetched transformed data for token {output_mint!r}." - ) - return False - - # the following are always passed to a strategy script, which may choose to ignore any - kwargs: Dict[str, Any] = self.params.strategies_kwargs - kwargs.update( - { - STRATEGY_KEY: self.synchronized_data.selected_strategy, - CALLABLE_KEY: EVALUATE_CALLABLE_KEY, - # TODO it is not clear which asset's data we should pass here - # shouldn't the evaluate method take both input and output token's data into account? - TRANSFORMED_PRICE_DATA_KEY: token_data, - ASSET_KEY: output_mint, - } - ) - results = self.execute_strategy_callable(**kwargs) - if results is None: - self.context.logger.error( - f"Something went wrong while backtesting token {output_mint!r}." - ) - return False - self.log_from_strategy_results(results) - sharpe: Optional[float] = results.get(BACKTEST_RESULT_KEY, None) - if sharpe is None or not isinstance(sharpe, float): - self.context.logger.error( - f"No float sharpe value can be extracted using key {BACKTEST_RESULT_KEY!r} in strategy's {results=}." - ) - return False - - self.context.logger.info(f"{sharpe=}.") - return sharpe >= self.params.sharpe_threshold - - def filter_orders( - self, orders: List[Dict[str, str]] - ) -> Generator[None, None, Tuple[List[Dict[str, str]], bool]]: - """Backtest the swap(s) and decide whether we should proceed to perform them or not.""" - transformed_data = yield from self.get_from_ipfs( - self.synchronized_data.transformed_data_hash, SupportedFiletype.JSON - ) - transformed_data = cast(Optional[Dict[str, Any]], transformed_data) - if transformed_data is None: - self.context.logger.error("Could not get the transformed data from IPFS.") - # return empty orders and incomplete status, because the transformed data are necessary for the backtesting - return [], True - - self.context.logger.info( - f"Using trading strategy {self.synchronized_data.selected_strategy!r} for backtesting..." - ) - - success_orders = [] - incomplete = False - for order in orders: - token = order.get(OUTPUT_MINT, None) - if token is None: - err = f"{OUTPUT_MINT!r} key was not found in {order=}." - self.context.logger.error(err) - incomplete = True - continue - - backtest_passed = self.backtest(transformed_data, token) - if backtest_passed: - success_orders.append(order) - continue - - incomplete = True - - if len(success_orders) == 0: - incomplete = True - - return success_orders, incomplete - - def async_act(self) -> Generator: - """Do the action.""" - yield from self.get_process_store_act( - self.synchronized_data.orders_hash, - self.filter_orders, - str(self.swap_decision_filepath), - ) diff --git a/packages/valory/skills/strategy_evaluator_abci/behaviours/base.py b/packages/valory/skills/strategy_evaluator_abci/behaviours/base.py deleted file mode 100644 index 3a599d4..0000000 --- a/packages/valory/skills/strategy_evaluator_abci/behaviours/base.py +++ /dev/null @@ -1,293 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the base behaviour for the 'strategy_evaluator_abci' skill.""" - -import json -from abc import ABC -from pathlib import Path -from typing import Any, Callable, Dict, Generator, Optional, Sized, Tuple, cast - -from packages.valory.skills.abstract_round_abci.base import BaseTxPayload -from packages.valory.skills.abstract_round_abci.behaviour_utils import BaseBehaviour -from packages.valory.skills.abstract_round_abci.io_.load import CustomLoaderType -from packages.valory.skills.abstract_round_abci.io_.store import ( - SupportedFiletype, - SupportedObjectType, -) -from packages.valory.skills.abstract_round_abci.models import ApiSpecs -from packages.valory.skills.strategy_evaluator_abci.models import ( - SharedState, - StrategyEvaluatorParams, -) -from packages.valory.skills.strategy_evaluator_abci.payloads import IPFSHashPayload -from packages.valory.skills.strategy_evaluator_abci.states.base import SynchronizedData - - -SWAP_DECISION_FILENAME = "swap_decision.json" -SWAP_INSTRUCTIONS_FILENAME = "swap_instructions.json" -STRATEGY_KEY = "trading_strategy" -CALLABLE_KEY = "callable" -ENTRY_POINT_STORE_KEY = "entry_point" -SUPPORTED_STRATEGY_LOG_LEVELS = ("info", "warning", "error") - - -def wei_to_native(wei: int) -> float: - """Convert WEI to native token.""" - return wei / 10**18 - - -def to_content(content: dict) -> bytes: - """Convert the given content to bytes' payload.""" - return json.dumps(content, sort_keys=True).encode() - - -class StrategyEvaluatorBaseBehaviour(BaseBehaviour, ABC): - """Represents the base class for the strategy evaluation FSM behaviour.""" - - def __init__(self, **kwargs: Any) -> None: - """Initialize the strategy evaluator behaviour.""" - super().__init__(**kwargs) - self.swap_decision_filepath = ( - Path(self.context.data_dir) / SWAP_DECISION_FILENAME - ) - self.swap_instructions_filepath = ( - Path(self.context.data_dir) / SWAP_INSTRUCTIONS_FILENAME - ) - self.token_balance = 0 - self.wallet_balance = 0 - - @property - def params(self) -> StrategyEvaluatorParams: - """Return the params.""" - return cast(StrategyEvaluatorParams, self.context.params) - - @property - def shared_state(self) -> SharedState: - """Get the shared state.""" - return cast(SharedState, self.context.state) - - @property - def synchronized_data(self) -> SynchronizedData: - """Return the synchronized data.""" - return SynchronizedData(super().synchronized_data.db) - - def strategy_store(self, strategy_name: str) -> Dict[str, str]: - """Get the stored strategy's files.""" - return self.context.shared_state.get(strategy_name, {}) - - def execute_strategy_callable( - self, *args: Any, **kwargs: Any - ) -> Dict[str, Any] | None: - """Execute a strategy's method and return the results.""" - trading_strategy: Optional[str] = kwargs.pop(STRATEGY_KEY, None) - if trading_strategy is None: - self.context.logger.error(f"No {STRATEGY_KEY!r} was given!") - return None - - callable_key: Optional[str] = kwargs.pop(CALLABLE_KEY, None) - if callable_key is None: - self.context.logger.error(f"No {CALLABLE_KEY!r} was given!") - return None - - store = self.strategy_store(trading_strategy) - strategy_exec = store.get(ENTRY_POINT_STORE_KEY, None) - if strategy_exec is None: - self.context.logger.error( - f"No executable was found for {trading_strategy=}! Did the IPFS package downloader load it correctly?" - ) - return None - - callable_method = store.get(callable_key, None) - if callable_method is None: - self.context.logger.error( - f"No {callable_method=} was found in the loaded component! " - "Did the IPFS package downloader load it correctly?" - ) - return None - - if callable_method in globals(): - del globals()[callable_method] - - exec(strategy_exec, globals()) # pylint: disable=W0122 # nosec - method: Optional[Callable] = globals().get(callable_method, None) - if method is None: - self.context.logger.error( - f"No {callable_method!r} method was found in {trading_strategy} strategy's executable:\n" - f"{strategy_exec}." - ) - return None - # TODO this method is blocking, needs to be run from an aea skill or a task. - return method(*args, **kwargs) - - def log_from_strategy_results(self, results: Dict[str, Any]) -> None: - """Log any messages from a strategy's results.""" - for level in SUPPORTED_STRATEGY_LOG_LEVELS: - logger = getattr(self.context.logger, level, None) - if logger is not None: - for log in results.get(level, []): - logger(log) - - def _handle_response( - self, - api: ApiSpecs, - res: Optional[dict], - ) -> Generator[None, None, Optional[Any]]: - """Handle the response from an API. - - :param api: the `ApiSpecs` instance of the API. - :param res: the response to handle. - :return: the response's result, using the given keys. `None` if response is `None` (has failed). - :yield: None - """ - if res is None: - error = f"Could not get a response from {api.api_id!r} API." - self.context.logger.error(error) - api.increment_retries() - yield from self.sleep(api.retries_info.suggested_sleep_time) - return None - - self.context.logger.info( - f"Retrieved a response from {api.api_id!r} API: {res}." - ) - api.reset_retries() - return res - - def _get_response( - self, - api: ApiSpecs, - dynamic_parameters: Dict[str, str], - content: Optional[dict] = None, - ) -> Generator[None, None, Any]: - """Get the response from an API.""" - specs = api.get_spec() - specs["parameters"].update(dynamic_parameters) - if content is not None: - specs["content"] = to_content(content) - - while not api.is_retries_exceeded(): - res_raw = yield from self.get_http_response(**specs) - res = api.process_response(res_raw) - response = yield from self._handle_response(api, res) - if response is not None: - return response - - error = f"Retries were exceeded for {api.api_id!r} API." - self.context.logger.error(error) - api.reset_retries() - return None - - def get_from_ipfs( - self, - ipfs_hash: Optional[str], - filetype: Optional[SupportedFiletype] = None, - custom_loader: CustomLoaderType = None, - timeout: Optional[float] = None, - ) -> Generator[None, None, Optional[SupportedObjectType]]: - """ - Gets an object from IPFS. - - If the result is `None`, then an error is logged, sleeps, and retries. - - :param ipfs_hash: the ipfs hash of the file/dir to download. - :param filetype: the file type of the object being downloaded. - :param custom_loader: a custom deserializer for the object received from IPFS. - :param timeout: timeout for the request. - :yields: None. - :returns: the downloaded object, corresponding to ipfs_hash or `None` if retries were exceeded. - """ - if ipfs_hash is None: - return None - - n_retries = 0 - while n_retries < self.params.ipfs_fetch_retries: - res = yield from super().get_from_ipfs( - ipfs_hash, filetype, custom_loader, timeout - ) - if res is not None: - return res - - n_retries += 1 - sleep_time = self.params.sleep_time - self.context.logger.error( - f"Could not get any data from IPFS using hash {ipfs_hash!r}!" - f"Retrying in {sleep_time}..." - ) - yield from self.sleep(sleep_time) - - return None - - def get_ipfs_hash_payload_content( - self, - data: Any, - process_fn: Callable[[Any], Generator[None, None, Tuple[Sized, bool]]], - store_filepath: str, - ) -> Generator[None, None, Tuple[Optional[str], Optional[bool]]]: - """Get the ipfs hash payload's content.""" - if data is None: - return None, None - - incomplete: Optional[bool] - processed, incomplete = yield from process_fn(data) - if len(processed) == 0: - processed_hash = None - if incomplete: - incomplete = None - else: - processed_hash = yield from self.send_to_ipfs( - store_filepath, - processed, - filetype=SupportedFiletype.JSON, - ) - return processed_hash, incomplete - - def get_process_store_act( - self, - hash_: Optional[str], - process_fn: Callable[[Any], Generator[None, None, Tuple[Sized, bool]]], - store_filepath: str, - ) -> Generator: - """An async act method for getting some data, processing them, and storing the result. - - 1. Get some data using the given hash. - 2. Process them using the given fn. - 3. Send them to IPFS using the given filepath as intermediate storage. - - :param hash_: the hash of the data to process. - :param process_fn: the function to process the data. - :param store_filepath: path to the file to store the processed data. - :yield: None - """ - with self.context.benchmark_tool.measure(self.behaviour_id).local(): - data = yield from self.get_from_ipfs(hash_, SupportedFiletype.JSON) - sender = self.context.agent_address - payload_data = yield from self.get_ipfs_hash_payload_content( - data, process_fn, store_filepath - ) - payload = IPFSHashPayload(sender, *payload_data) - - yield from self.finish_behaviour(payload) - - def finish_behaviour(self, payload: BaseTxPayload) -> Generator: - """Finish the behaviour.""" - with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): - yield from self.send_a2a_transaction(payload) - yield from self.wait_until_round_end() - - self.set_done() diff --git a/packages/valory/skills/strategy_evaluator_abci/behaviours/prepare_swap_tx.py b/packages/valory/skills/strategy_evaluator_abci/behaviours/prepare_swap_tx.py deleted file mode 100644 index b83a7f4..0000000 --- a/packages/valory/skills/strategy_evaluator_abci/behaviours/prepare_swap_tx.py +++ /dev/null @@ -1,412 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the behaviour for preparing swap(s) instructions.""" - -import json -import traceback -from typing import Any, Callable, Dict, Generator, List, Optional, Sized, Tuple, cast - -from packages.eightballer.connections.dcxt import PUBLIC_ID as DCXT_ID -from packages.eightballer.protocols.orders.custom_types import ( - Order, - OrderSide, - OrderType, -) -from packages.eightballer.protocols.orders.message import OrdersMessage -from packages.valory.contracts.gnosis_safe.contract import GnosisSafeContract -from packages.valory.protocols.contract_api.message import ContractApiMessage -from packages.valory.skills.abstract_round_abci.base import ( - BaseTxPayload, - LEDGER_API_ADDRESS, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - ContractApiDialogue, - ContractApiDialogues, -) -from packages.valory.skills.abstract_round_abci.io_.store import SupportedFiletype -from packages.valory.skills.abstract_round_abci.models import Requests -from packages.valory.skills.strategy_evaluator_abci.behaviours.base import ( - StrategyEvaluatorBaseBehaviour, -) -from packages.valory.skills.strategy_evaluator_abci.payloads import ( - TransactionHashPayload, -) -from packages.valory.skills.strategy_evaluator_abci.states.prepare_swap import ( - PrepareEvmSwapRound, - PrepareSwapRound, -) -from packages.valory.skills.transaction_settlement_abci.payload_tools import ( - hash_payload_to_hex, -) -from packages.valory.skills.transaction_settlement_abci.rounds import TX_HASH_LENGTH - - -SAFE_GAS = 0 - - -class PrepareSwapBehaviour(StrategyEvaluatorBaseBehaviour): - """A behaviour in which the agents execute the selected strategy and decide on the swap(s).""" - - matching_round = PrepareSwapRound - - def __init__(self, **kwargs: Any): - """Initialize the swap-preparation behaviour.""" - super().__init__(**kwargs) - self.incomplete = False - - def setup(self) -> None: - """Initialize the behaviour.""" - self.context.swap_quotes.reset_retries() - self.context.swap_instructions.reset_retries() - - def build_quote( - self, quote_data: Dict[str, str] - ) -> Generator[None, None, Optional[dict]]: - """Build the quote.""" - response = yield from self._get_response(self.context.swap_quotes, quote_data) - return response - - def build_instructions(self, quote: dict) -> Generator[None, None, Optional[dict]]: - """Build the instructions.""" - content = { - "quoteResponse": quote, - "userPublicKey": self.context.agent_address, - } - response = yield from self._get_response( - self.context.swap_instructions, - dynamic_parameters={}, - content=content, - ) - return response - - def build_swap_tx( - self, quote_data: Dict[str, str] - ) -> Generator[None, None, Optional[Dict[str, Any]]]: - """Build instructions for a swap transaction.""" - quote = yield from self.build_quote(quote_data) - if quote is None: - return None - instructions = yield from self.build_instructions(quote) - return instructions - - def prepare_instructions( - self, orders: List[Dict[str, str]] - ) -> Generator[None, None, Tuple[List[Dict[str, Any]], bool]]: - """Prepare the instructions for a Swap transaction.""" - instructions = [] - for quote_data in orders: - swap_instruction = yield from self.build_swap_tx(quote_data) - if swap_instruction is None: - self.incomplete = True - else: - instructions.append(swap_instruction) - - return instructions, self.incomplete - - def async_act(self) -> Generator: - """Do the action.""" - yield from self.get_process_store_act( - self.synchronized_data.backtested_orders_hash, - self.prepare_instructions, - str(self.swap_instructions_filepath), - ) - - -class PrepareEvmSwapBehaviour(StrategyEvaluatorBaseBehaviour): - """A behaviour in which the agents execute the selected strategy and decide on the swap(s).""" - - matching_round = PrepareEvmSwapRound - - def __init__(self, **kwargs: Any): - """Initialize the swap-preparation behaviour.""" - super().__init__(**kwargs) - self.incomplete = False - self._performative_to_dialogue_class = { - OrdersMessage.Performative.CREATE_ORDER: self.context.orders_dialogues, - } - - def setup(self) -> None: - """Initialize the behaviour.""" - self.context.swap_quotes.reset_retries() - self.context.swap_instructions.reset_retries() - - def build_quote( - self, quote_data: Dict[str, str] - ) -> Generator[None, None, Optional[dict]]: - """Build the quote.""" - response = yield from self._get_response(self.context.swap_quotes, quote_data) - return response - - def build_instructions(self, quote: dict) -> Generator[None, None, Optional[dict]]: - """Build the instructions.""" - content = { - "quoteResponse": quote, - "userPublicKey": self.context.agent_address, - } - response = yield from self._get_response( - self.context.swap_instructions, - dynamic_parameters={}, - content=content, - ) - return response - - def build_swap_tx( - self, quote_data: Dict[str, str] - ) -> Generator[None, None, Optional[Dict[str, Any]]]: - """Build instructions for a swap transaction.""" - quote = yield from self.build_quote(quote_data) - if quote is None: - return None - instructions = yield from self.build_instructions(quote) - return instructions - - def prepare_transactions( - self, orders: List[Dict[str, str]] - ) -> Generator[None, None, Tuple[List[Dict[str, Any]], bool]]: - """Prepare the instructions for a Swap transaction.""" - instructions = [] - for quote_data in orders: - symbol = f'{quote_data["inputMint"]}/{quote_data["outputMint"]}' - # We assume for now that we are only sending to the one exchange - ledger_id: str = self.params.ledger_ids[0] - exchange_ids = self.params.exchange_ids[ledger_id] - if len(exchange_ids) != 1: - self.context.logger.error( - f"Expected exactly one exchange id, got {exchange_ids}." - ) - raise ValueError( - f"Expected exactly one exchange id, got {exchange_ids}." - ) - exchange_id = f"{exchange_ids[0]}_{ledger_id}" - - order = Order( - exchange_id=exchange_id, - symbol=symbol, - amount=self.params.trade_size_in_base_token, - side=OrderSide.BUY, - type=OrderType.MARKET, - data=json.dumps( - { - "safe_contract_address": self.synchronized_data.safe_contract_address, - } - ), - ) - - result = yield from self.get_dcxt_response( - protocol_performative=OrdersMessage.Performative.CREATE_ORDER, # type: ignore - order=order, - ) - call_data = result.order.data - try: - can_create_hash = yield from self._build_safe_tx_hash( - vault_address=call_data["vault_address"], - chain_id=call_data["chain_id"], - call_data=bytes.fromhex(call_data["data"][2:]), - ) - except Exception as e: - can_create_hash = False - self.context.logger.error( - f"Error building safe tx hash: {traceback.format_exc()} with error {e}" - ) - - if call_data is None: - self.incomplete = not can_create_hash - else: - instructions.append(call_data) - - return instructions, self.incomplete - - def _build_safe_tx_hash( - self, - vault_address: str, - chain_id: int, - call_data: bytes, - ) -> Any: - """Prepares and returns the safe tx hash for a multisend tx.""" - self.context.logger.info( - f"Building safe tx hash: safe={self.synchronized_data.safe_contract_address}\n" - + f"vault={vault_address}\n" - + f"chain_id={chain_id}\n" - + f"call_data={call_data.hex()}" - ) - - ledger_id: str = self.params.ledger_ids[0] - response_msg = yield from self.get_contract_api_response( - performative=ContractApiMessage.Performative.GET_RAW_TRANSACTION, # type: ignore - contract_address=self.synchronized_data.safe_contract_address, - contract_id=str(GnosisSafeContract.contract_id), - contract_callable="get_raw_safe_transaction_hash", - to_address=vault_address, - value=0, - data=call_data, - safe_tx_gas=SAFE_GAS, - ledger_id="ethereum", - chain_id=ledger_id, - ) - - if response_msg.performative != ContractApiMessage.Performative.RAW_TRANSACTION: - self.context.logger.error( - "Couldn't get safe tx hash. Expected response performative " - f"{ContractApiMessage.Performative.RAW_TRANSACTION.value}, " # type: ignore - f"received {response_msg.performative.value}: {response_msg}." - ) - return False - - tx_hash = response_msg.raw_transaction.body.get("tx_hash", None) - if tx_hash is None or len(tx_hash) != TX_HASH_LENGTH: - self.context.logger.error( - "Something went wrong while trying to get the buy transaction's hash. " - f"Invalid hash {tx_hash!r} was returned." - ) - return False - - safe_tx_hash = tx_hash[2:] - self.context.logger.info(f"Hash of the Safe transaction: {safe_tx_hash}") - # temp hack: - payload_string = hash_payload_to_hex( - safe_tx_hash, 0, SAFE_GAS, vault_address, call_data - ) - self.safe_tx_hash = safe_tx_hash - self.payload_string = payload_string - self.call_data = call_data - return True - - def async_act(self) -> Generator: - """Do the action.""" - yield from self.get_process_store_act( - self.synchronized_data.backtested_orders_hash, - self.prepare_transactions, - str(self.swap_instructions_filepath), - ) - - def get_dcxt_response( - self, - protocol_performative: OrdersMessage.Performative, - **kwargs: Any, - ) -> Generator[None, None, Any]: - """Get a ccxt response.""" - if protocol_performative not in self._performative_to_dialogue_class: - raise ValueError( - f"Unsupported protocol performative {protocol_performative}." - ) - dialogue_class = self._performative_to_dialogue_class[protocol_performative] - - msg, dialogue = dialogue_class.create( - counterparty=str(DCXT_ID), - performative=protocol_performative, - **kwargs, - ) - msg._sender = str(self.context.skill_id) # pylint: disable=protected-access - response = yield from self._do_request(msg, dialogue) - return response - - def get_process_store_act( - self, - hash_: Optional[str], - process_fn: Callable[[Any], Generator[None, None, Tuple[Sized, bool]]], - store_filepath: str, - ) -> Generator: - """An async act method for getting some data, processing them, and storing the result. - - 1. Get some data using the given hash. - 2. Process them using the given fn. - 3. Send them to IPFS using the given filepath as intermediate storage. - - :param hash_: the hash of the data to process. - :param process_fn: the function to process the data. - :param store_filepath: path to the file to store the processed data. - :yield: None - """ - with self.context.benchmark_tool.measure(self.behaviour_id).local(): - data = yield from self.get_from_ipfs(hash_, SupportedFiletype.JSON) - sender = self.context.agent_address - yield from self.get_ipfs_hash_payload_content( - data, process_fn, store_filepath - ) - - payload = TransactionHashPayload( - sender, - tx_hash=self.payload_string, - ) - - yield from self.finish_behaviour(payload) - - def finish_behaviour(self, payload: BaseTxPayload) -> Generator: - """Finish the behaviour.""" - with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): - yield from self.send_a2a_transaction(payload) - yield from self.wait_until_round_end() - - self.set_done() - - def get_contract_api_response( - self, - performative: ContractApiMessage.Performative, - contract_address: Optional[str], - contract_id: str, - contract_callable: str, - ledger_id: Optional[str] = None, - **kwargs: Any, - ) -> Generator[None, None, ContractApiMessage]: - """ - Request contract safe transaction hash - - Happy-path full flow of the messages. - - AbstractRoundAbci skill -> (ContractApiMessage | ContractApiMessage.Performative) -> Ledger connection (contract dispatcher) - Ledger connection (contract dispatcher) -> (ContractApiMessage | ContractApiMessage.Performative) -> AbstractRoundAbci skill - - :param performative: the message performative - :param contract_address: the contract address - :param contract_id: the contract id - :param contract_callable: the callable to call on the contract - :param ledger_id: the ledger id, if not specified, the default ledger id is used - :param kwargs: keyword argument for the contract api request - :return: the contract api response - :yields: the contract api response - """ - contract_api_dialogues = cast( - ContractApiDialogues, self.context.contract_api_dialogues - ) - kwargs = { - "performative": performative, - "counterparty": LEDGER_API_ADDRESS, - "ledger_id": ledger_id or self.context.default_ledger_id, - "contract_id": contract_id, - "callable": contract_callable, - "kwargs": ContractApiMessage.Kwargs(kwargs), - } - if contract_address is not None: - kwargs["contract_address"] = contract_address - contract_api_msg, contract_api_dialogue = contract_api_dialogues.create( - **kwargs - ) - contract_api_dialogue = cast( - ContractApiDialogue, - contract_api_dialogue, - ) - contract_api_dialogue.terms = self._get_default_terms() - request_nonce = self._get_request_nonce_from_dialogue(contract_api_dialogue) - cast(Requests, self.context.requests).request_id_to_callback[ - request_nonce - ] = self.get_callback_request() - self.context.outbox.put_message(message=contract_api_msg) - response = yield from self.wait_for_message() - return response diff --git a/packages/valory/skills/strategy_evaluator_abci/behaviours/proxy_swap_queue.py b/packages/valory/skills/strategy_evaluator_abci/behaviours/proxy_swap_queue.py deleted file mode 100644 index 2efdc3a..0000000 --- a/packages/valory/skills/strategy_evaluator_abci/behaviours/proxy_swap_queue.py +++ /dev/null @@ -1,147 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the behaviour for sending a transaction for the next swap in the queue of orders.""" - -from typing import Dict, Generator, List, Optional, cast - -from packages.valory.skills.abstract_round_abci.io_.store import SupportedFiletype -from packages.valory.skills.strategy_evaluator_abci.behaviours.base import ( - StrategyEvaluatorBaseBehaviour, -) -from packages.valory.skills.strategy_evaluator_abci.models import TxSettlementProxy -from packages.valory.skills.strategy_evaluator_abci.payloads import SendSwapProxyPayload -from packages.valory.skills.strategy_evaluator_abci.states.proxy_swap_queue import ( - ProxySwapQueueRound, -) - - -OrdersType = Optional[List[Dict[str, str]]] - - -PROXY_STATUS_FIELD = "status" -PROXY_TX_ID_FIELD = "txId" -PROXY_TX_URL_FIELD = "url" -PROXY_ERROR_MESSAGE_FIELD = "message" -PROXY_SUCCESS_RESPONSE = "ok" -PROXY_ERROR_RESPONSE = "error" - - -class ProxySwapQueueBehaviour(StrategyEvaluatorBaseBehaviour): - """A behaviour in which the agent utilizes the proxy server to perform the next swap transaction in priority. - - Warning: This can only work with a single agent service. - """ - - matching_round = ProxySwapQueueRound - - def setup(self) -> None: - """Initialize the behaviour.""" - self.context.tx_settlement_proxy.reset_retries() - - @property - def orders(self) -> OrdersType: - """Get the orders from the shared state.""" - return self.shared_state.orders - - @orders.setter - def orders(self, orders: OrdersType) -> None: - """Set the orders to the shared state.""" - self.shared_state.orders = orders - - def get_orders(self) -> Generator: - """Get the orders from IPFS.""" - if self.orders is None: - # only fetch once per new batch and store in the shared state for future reference - hash_ = self.synchronized_data.backtested_orders_hash - orders = yield from self.get_from_ipfs(hash_, SupportedFiletype.JSON) - self.orders = cast(OrdersType, orders) - - def handle_success(self, tx_id: Optional[str], url: Optional[str]) -> None: - """Handle a successful response.""" - if not tx_id: - err = "The proxy server returned no transaction id for successful transaction!" - self.context.logger.error(err) - - swap_msg = f"Successfully performed swap transaction with id {tx_id}" - swap_msg += f": {url}" if url is not None else "." - self.context.logger.info(swap_msg) - - def handle_error(self, err: str) -> None: - """Handle an error response.""" - err = f"Proxy server failed to settle transaction with message: {err}" - self.context.logger.error(err) - - def handle_unknown_status(self, status: Optional[str]) -> None: - """Handle a response with an unknown status.""" - err = f"Unknown {status=} was received from the transaction settlement proxy server!" - self.context.logger.error(err) - - def handle_response(self, response: Optional[Dict[str, str]]) -> Optional[str]: - """Handle the response from the proxy server.""" - self.context.logger.debug(f"Proxy server {response=}.") - if response is None: - return None - - status = response.get(PROXY_STATUS_FIELD, None) - tx_id = response.get(PROXY_TX_ID_FIELD, None) - if status == PROXY_SUCCESS_RESPONSE: - url = response.get(PROXY_TX_URL_FIELD, None) - self.handle_success(tx_id, url) - elif status == PROXY_ERROR_RESPONSE: - err = response.get(PROXY_ERROR_MESSAGE_FIELD, "") - self.handle_error(err) - else: - self.handle_unknown_status(status) - - return tx_id - - def perform_next_order(self) -> Generator[None, None, Optional[str]]: - """Perform the next order in priority and return the tx id or `None` if not sent.""" - if self.orders is None: - err = "Orders were expected to be set." - self.context.logger.error(err) - return None - - if len(self.orders) == 0: - self.context.logger.info("No more orders to process.") - self.orders = None - return "" - - quote_data = self.orders.pop(0) - msg = f"Attempting to swap {quote_data['inputMint']} -> {quote_data['outputMint']}..." - self.context.logger.info(msg) - - proxy_api = cast(TxSettlementProxy, self.context.tx_settlement_proxy) - # hacky solution - params = proxy_api.get_spec()["parameters"] - quote_data.update(params) - - response = yield from self._get_response(proxy_api, {}, content=quote_data) - return self.handle_response(response) - - def async_act(self) -> Generator: - """Do the action.""" - with self.context.benchmark_tool.measure(self.behaviour_id).local(): - yield from self.get_orders() - sender = self.context.agent_address - tx_id = yield from self.perform_next_order() - payload = SendSwapProxyPayload(sender, tx_id) - - yield from self.finish_behaviour(payload) diff --git a/packages/valory/skills/strategy_evaluator_abci/behaviours/round_behaviour.py b/packages/valory/skills/strategy_evaluator_abci/behaviours/round_behaviour.py deleted file mode 100644 index cf0b67b..0000000 --- a/packages/valory/skills/strategy_evaluator_abci/behaviours/round_behaviour.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the round behaviour for the 'strategy_evaluator_abci' skill.""" - -from typing import Set, Type - -from packages.valory.skills.abstract_round_abci.behaviours import ( - AbstractRoundBehaviour, - BaseBehaviour, -) -from packages.valory.skills.strategy_evaluator_abci.behaviours.backtesting import ( - BacktestBehaviour, -) -from packages.valory.skills.strategy_evaluator_abci.behaviours.prepare_swap_tx import ( - PrepareEvmSwapBehaviour, - PrepareSwapBehaviour, -) -from packages.valory.skills.strategy_evaluator_abci.behaviours.proxy_swap_queue import ( - ProxySwapQueueBehaviour, -) -from packages.valory.skills.strategy_evaluator_abci.behaviours.strategy_exec import ( - StrategyExecBehaviour, -) -from packages.valory.skills.strategy_evaluator_abci.behaviours.swap_queue import ( - SwapQueueBehaviour, -) -from packages.valory.skills.strategy_evaluator_abci.rounds import ( - StrategyEvaluatorAbciApp, -) - - -class AgentStrategyEvaluatorRoundBehaviour(AbstractRoundBehaviour): - """This behaviour manages the consensus stages for the strategy evaluation.""" - - initial_behaviour_cls = StrategyExecBehaviour - abci_app_cls = StrategyEvaluatorAbciApp - behaviours: Set[Type[BaseBehaviour]] = { - StrategyExecBehaviour, # type: ignore - PrepareSwapBehaviour, # type: ignore - PrepareEvmSwapBehaviour, # type: ignore - SwapQueueBehaviour, # type: ignore - ProxySwapQueueBehaviour, # type: ignore - BacktestBehaviour, # type: ignore - } diff --git a/packages/valory/skills/strategy_evaluator_abci/behaviours/strategy_exec.py b/packages/valory/skills/strategy_evaluator_abci/behaviours/strategy_exec.py deleted file mode 100644 index 4e9e54c..0000000 --- a/packages/valory/skills/strategy_evaluator_abci/behaviours/strategy_exec.py +++ /dev/null @@ -1,351 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the behaviour for executing a strategy.""" - -from typing import Any, Dict, Generator, List, Optional, Tuple, cast - -from packages.valory.skills.abstract_round_abci.io_.store import SupportedFiletype -from packages.valory.skills.portfolio_tracker_abci.behaviours import SOL_ADDRESS -from packages.valory.skills.strategy_evaluator_abci.behaviours.base import ( - CALLABLE_KEY, - StrategyEvaluatorBaseBehaviour, -) -from packages.valory.skills.strategy_evaluator_abci.models import AMOUNT_PARAM -from packages.valory.skills.strategy_evaluator_abci.states.strategy_exec import ( - StrategyExecRound, -) - - -STRATEGY_KEY = "trading_strategy" -PRICE_DATA_KEY = "price_data" -TRANSFORMED_PRICE_DATA_KEY = "transformed_data" -TOKEN_ID_KEY = "token_id" # nosec B105:hardcoded_password_string -PORTFOLIO_DATA_KEY = "portfolio_data" -SWAP_DECISION_FIELD = "signal" -BUY_DECISION = "buy" -SELL_DECISION = "sell" -HODL_DECISION = "hold" -AVAILABLE_DECISIONS = (BUY_DECISION, SELL_DECISION, HODL_DECISION) -NO_SWAP_DECISION = {SWAP_DECISION_FIELD: HODL_DECISION} -SOL = "SOL" -RUN_CALLABLE_KEY = "run_callable" -INPUT_MINT = "inputMint" -OUTPUT_MINT = "outputMint" - - -class StrategyExecBehaviour(StrategyEvaluatorBaseBehaviour): - """A behaviour in which the agents execute the selected strategy and decide on the swap(s).""" - - matching_round = StrategyExecRound - - def __init__(self, **kwargs: Any): - """Initialize the behaviour.""" - super().__init__(**kwargs) - self.sol_balance: int = 0 - self.sol_balance_after_swaps: int = 0 - - def get_swap_amount(self) -> int: - """Get the swap amount.""" - if self.params.use_proxy_server: - api = self.context.tx_settlement_proxy - else: - api = self.context.swap_quotes - - return api.parameters.get(AMOUNT_PARAM, 0) - - def is_balance_sufficient( - self, - token: str, - token_balance: int, - ) -> Optional[bool]: - """Check whether the balance of the given token is enough to perform the swap transaction.""" - if token == SOL_ADDRESS and self.sol_balance_after_swaps <= 0: - warning = "Preceding trades are expected to use up all the SOL. Not taking any action." - self.context.logger.warning(warning) - return False - - swap_cost = self.params.expected_swap_tx_cost - # we set it to `None` if no swaps have been prepared yet - sol_before_swap = ( - None - if self.sol_balance_after_swaps == self.sol_balance - else self.sol_balance_after_swaps - ) - if swap_cost > self.sol_balance_after_swaps: - self.context.logger.warning( - "There is not enough SOL to cover the expected swap tx's cost. " - f"SOL balance after preceding swaps ({self.sol_balance_after_swaps}) < swap cost ({swap_cost}). " - f"Not taking any actions." - ) - return False - self.sol_balance_after_swaps -= swap_cost - - swap_amount = self.get_swap_amount() - if token == SOL_ADDRESS: - # do not use the SOL's address to simplify the log messages - token = SOL - compared_balance = self.sol_balance_after_swaps - self.sol_balance_after_swaps -= swap_amount - else: - compared_balance = token_balance - - self.context.logger.info(f"Balance ({token}): {token_balance}.") - if swap_amount > compared_balance: - warning = ( - f"There is not enough balance to cover the swap amount ({swap_amount}) " - ) - if token == SOL: - # subtract the SOL we'd have before this swap or the token's balance if there are no preceding swaps - preceding_swaps_amount = token_balance - ( - sol_before_swap or token_balance - ) - - warning += ( - f"plus the expected swap tx's cost ({swap_cost}) [" - f"also taking into account preceding swaps' amount ({preceding_swaps_amount})] " - ) - # the swap's cost which was subtracted during the first `get_native_balance` call - # should be included in the swap amount - self.sol_balance_after_swaps += swap_amount - swap_amount += swap_cost - token_balance -= preceding_swaps_amount - self.sol_balance_after_swaps += swap_cost - warning += f"({token_balance} < {swap_amount}) for {token!r}. Not taking any actions." - self.context.logger.warning(warning) - return False - - self.context.logger.info("Balance is sufficient.") - return True - - def get_swap_decision( - self, - token_data: Any, - portfolio_data: Dict[str, int], - token: str, - ) -> Optional[str]: - """Get the swap decision given a token's data.""" - strategy = self.synchronized_data.selected_strategy - self.context.logger.info(f"Using trading strategy {strategy!r}.") - # the following are always passed to a strategy script, which may choose to ignore any - kwargs: Dict[str, Any] = self.params.strategies_kwargs - kwargs.update( - { - STRATEGY_KEY: strategy, - CALLABLE_KEY: RUN_CALLABLE_KEY, - TRANSFORMED_PRICE_DATA_KEY: token_data, - PORTFOLIO_DATA_KEY: portfolio_data, - TOKEN_ID_KEY: token, - } - ) - results = self.execute_strategy_callable(**kwargs) - if results is None: - results = NO_SWAP_DECISION - - self.log_from_strategy_results(results) - decision = results.get(SWAP_DECISION_FIELD, None) - if decision is None: - self.context.logger.error( - f"Required field {SWAP_DECISION_FIELD!r} was not returned by {strategy} strategy." - "Not taking any actions." - ) - if decision not in AVAILABLE_DECISIONS: - self.context.logger.error( - f"Invalid decision {decision!r} was detected! Expected one of {AVAILABLE_DECISIONS}." - "Not taking any actions." - ) - decision = None - - return decision - - def get_token_swap_position(self, decision: str) -> Optional[str]: - """Get the position of the non-native token in the swap operation.""" - token_swap_position = None - - if decision == BUY_DECISION: - token_swap_position = OUTPUT_MINT - elif decision == SELL_DECISION: - token_swap_position = INPUT_MINT - elif decision != HODL_DECISION: - self.context.logger.error( - f"Unrecognised decision {decision!r} found! Expected one of {AVAILABLE_DECISIONS}." - ) - - return token_swap_position - - def get_solana_orders( - self, token_data: Dict[str, Any] - ) -> Generator[None, None, Tuple[List[Dict[str, str]], bool]]: - """Get a mapping from a string indicating whether to buy or sell, to a list of tokens.""" - portfolio = yield from self.get_from_ipfs( - self.synchronized_data.portfolio_hash, SupportedFiletype.JSON - ) - portfolio = cast(Optional[Dict[str, int]], portfolio) - if portfolio is None: - self.context.logger.error("Could not get the portfolio from IPFS.") - # return empty orders and incomplete status, because the portfolio is necessary for all the swaps - return [], True - - sol_balance = portfolio.get(SOL_ADDRESS, None) - if sol_balance is None: - err = "The portfolio data do not contain any information for SOL." - self.context.logger.error(err) - # return empty orders and incomplete status, because SOL are necessary for all the swaps - return [], True - self.sol_balance = self.sol_balance_after_swaps = sol_balance - - orders: List[Dict[str, str]] = [] - incomplete = False - for token, data in token_data.items(): - if token == SOL_ADDRESS: - continue - - decision = self.get_swap_decision(data, portfolio, token) - if decision is None: - incomplete = True - continue - - msg = f"Decided to {decision} token with address {token!r}." - self.context.logger.info(msg) - quote_data = {INPUT_MINT: SOL_ADDRESS, OUTPUT_MINT: SOL_ADDRESS} - token_swap_position = self.get_token_swap_position(decision) - if token_swap_position is None: - # holding token, no tx to perform - continue - - quote_data[token_swap_position] = token - input_token = quote_data[INPUT_MINT] - if input_token is not SOL: - token_balance = portfolio.get(input_token, None) - if token_balance is None: - err = f"The portfolio data do not contain any information for {token!r}." - self.context.logger.error(err) - # return, because a swap for another token might be performed - continue - else: - token_balance = self.sol_balance - - enough_tokens = self.is_balance_sufficient(input_token, token_balance) - if not enough_tokens: - incomplete = True - continue - orders.append(quote_data) - - # we only yield here to convert this method to a generator, so that it can be used by `get_process_store_act` - yield - return orders, incomplete - - def get_evm_orders( - self, - token_data: Dict[str, Any], - required_amount: int = 0.00001, - ) -> Generator[None, None, Tuple[List[Dict[str, str]], bool]]: - """Get a mapping from a string indicating whether to buy or sell, to a list of tokens.""" - # We need to check if the portfolio contains any information for the NATIVE_TOKEN which is yet to be defined - # We will temporarily skip this check - # TODO: Define NATIVE_TOKEN, BASE_TOKEN, and LEDGER_ID - # TODO: Check if the portfolio contains any information for the NATIVE_TOKEN - # TODO: Mapping of ledger id to base token - # TODO: update evm balance checker to include native token balance in the portfolio. - - portfolio: Optional[Dict[str, int]] = yield from self.get_from_ipfs( # type: ignore - self.synchronized_data.portfolio_hash, SupportedFiletype.JSON - ) - ledger_id: str = self.params.ledger_ids[0] - base_token = self.params.base_tokens.get(ledger_id, None) - if base_token is None: - self.context.logger.error( - f"Could not get the base token for ledger {ledger_id!r} from the configuration." - ) - return [], True - native_token = self.params.native_currencies.get(ledger_id, None) - if native_token is None: - self.context.logger.error( - f"Could not get the native token for ledger {ledger_id!r} from the configuration." - ) - return [], True - - if portfolio is None: - self.context.logger.error("Could not get the portfolio from IPFS.") - # return empty orders and incomplete status, because the portfolio is necessary for all the swaps - return [], True - - native_balance = portfolio.get(native_token, None) - if native_balance is None: - err = f"The portfolio data do not contain any information for the native {native_token!r} on ledger {ledger_id!r}." - self.context.logger.error(err) - # return empty orders and incomplete status, because SOL are necessary for all the swaps - return [], True - - orders: List[Dict[str, str]] = [] - incomplete = False - for token, data in token_data.items(): - if token == base_token or token == native_token: - continue - - decision = self.get_swap_decision(data, portfolio, token) - if decision is None: - incomplete = True - continue - - msg = f"Decided to {decision} token with address {token!r}." - self.context.logger.info(msg) - token_swap_position = self.get_token_swap_position(decision) - if token_swap_position is None: - # holding token, no tx to perform - continue - - input_token = base_token - output_token = token - - input_balance = portfolio.get(input_token, None) - if input_balance is None: - err = f"The portfolio does not contain information for the base token {base_token!r}. The base token is required for all swaps." - self.context.logger.error(err) - # return, because a swap for another token might be performed - continue - - if input_token == native_token: - incomplete = True - continue - - if input_balance < int(required_amount): - err = f"The portfolio does not contain enough balance for the base token {base_token!r}. Current balance: {input_balance}. Required amount: {required_amount}." - self.context.logger.error(err) - breakpoint() - incomplete = True - continue - quote_data = {INPUT_MINT: input_token, OUTPUT_MINT: output_token} - orders.append(quote_data) - - # we only yield here to convert this method to a generator, so that it can be used by `get_process_store_act` - yield - return orders, incomplete - - def async_act(self) -> Generator: - """Do the action.""" - # We check if we are processing the solana or the ethereum chain - processing_function = ( - self.get_solana_orders if self.params.use_solana else self.get_evm_orders - ) - yield from self.get_process_store_act( - self.synchronized_data.transformed_data_hash, - processing_function, # type: ignore - str(self.swap_decision_filepath), - ) diff --git a/packages/valory/skills/strategy_evaluator_abci/behaviours/swap_queue.py b/packages/valory/skills/strategy_evaluator_abci/behaviours/swap_queue.py deleted file mode 100644 index daa680f..0000000 --- a/packages/valory/skills/strategy_evaluator_abci/behaviours/swap_queue.py +++ /dev/null @@ -1,91 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the behaviour for preparing a transaction for the next swap in the queue of instructions.""" - -import json -from typing import Any, Dict, Generator, List, Optional, cast - -from packages.valory.skills.abstract_round_abci.io_.store import SupportedFiletype -from packages.valory.skills.strategy_evaluator_abci.behaviours.base import ( - StrategyEvaluatorBaseBehaviour, -) -from packages.valory.skills.strategy_evaluator_abci.payloads import SendSwapPayload -from packages.valory.skills.strategy_evaluator_abci.states.swap_queue import ( - SwapQueueRound, -) - - -class SwapQueueBehaviour(StrategyEvaluatorBaseBehaviour): - """A behaviour in which the agents prepare a transaction for the next swap in the queue of instructions.""" - - matching_round = SwapQueueRound - - @property - def instructions(self) -> Optional[List[Dict[str, Any]]]: - """Get the instructions from the shared state.""" - return self.shared_state.instructions - - @instructions.setter - def instructions(self, instructions: Optional[List[Dict[str, Any]]]) -> None: - """Set the instructions to the shared state.""" - self.shared_state.instructions = instructions - - def get_instructions(self) -> Generator: - """Get the instructions from IPFS.""" - if self.instructions is None: - # only fetch once per new queue and store in the shared state for future reference - hash_ = self.synchronized_data.instructions_hash - instructions = yield from self.get_from_ipfs(hash_, SupportedFiletype.JSON) - self.instructions = cast(Optional[List[Dict[str, Any]]], instructions) - - def get_next_instructions(self) -> Optional[str]: - """Return the next instructions in priority serialized or `None` if there are no instructions left.""" - if self.instructions is None: - err = "Instructions were expected to be set." - self.context.logger.error(err) - return None - - if len(self.instructions) == 0: - self.context.logger.info("No more instructions to process.") - self.instructions = None - return "" - - instructions = self.instructions.pop(0) - if len(instructions) == 0: - err = "The next instructions in priority are not correctly set! Skipping them..." - self.context.logger.error(err) - return None - - try: - return json.dumps(instructions) - except (json.decoder.JSONDecodeError, TypeError): - err = "The next instructions in priority are not correctly formatted! Skipping them..." - self.context.logger.error(err) - return None - - def async_act(self) -> Generator: - """Do the action.""" - with self.context.benchmark_tool.measure(self.behaviour_id).local(): - yield from self.get_instructions() - sender = self.context.agent_address - serialized_instructions = self.get_next_instructions() - payload = SendSwapPayload(sender, serialized_instructions) - - yield from self.finish_behaviour(payload) diff --git a/packages/valory/skills/strategy_evaluator_abci/dialogues.py b/packages/valory/skills/strategy_evaluator_abci/dialogues.py deleted file mode 100644 index 16b98c0..0000000 --- a/packages/valory/skills/strategy_evaluator_abci/dialogues.py +++ /dev/null @@ -1,100 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the classes required for dialogue management.""" - -from packages.eightballer.protocols.orders.dialogues import ( - OrdersDialogue as BaseOrdersDialogue, -) -from packages.eightballer.protocols.orders.dialogues import ( - OrdersDialogues as BaseOrdersDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - AbciDialogue as BaseAbciDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - AbciDialogues as BaseAbciDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - ContractApiDialogue as BaseContractApiDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - ContractApiDialogues as BaseContractApiDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - HttpDialogue as BaseHttpDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - HttpDialogues as BaseHttpDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - IpfsDialogue as BaseIpfsDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - IpfsDialogues as BaseIpfsDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - LedgerApiDialogue as BaseLedgerApiDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - LedgerApiDialogues as BaseLedgerApiDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - SigningDialogue as BaseSigningDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - SigningDialogues as BaseSigningDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - TendermintDialogue as BaseTendermintDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - TendermintDialogues as BaseTendermintDialogues, -) - - -AbciDialogue = BaseAbciDialogue -AbciDialogues = BaseAbciDialogues - - -HttpDialogue = BaseHttpDialogue -HttpDialogues = BaseHttpDialogues - - -SigningDialogue = BaseSigningDialogue -SigningDialogues = BaseSigningDialogues - - -LedgerApiDialogue = BaseLedgerApiDialogue -LedgerApiDialogues = BaseLedgerApiDialogues - - -ContractApiDialogue = BaseContractApiDialogue -ContractApiDialogues = BaseContractApiDialogues - - -TendermintDialogue = BaseTendermintDialogue -TendermintDialogues = BaseTendermintDialogues - - -IpfsDialogue = BaseIpfsDialogue -IpfsDialogues = BaseIpfsDialogues - -DcxtOrdersDialogue = BaseOrdersDialogue -DcxtOrdersDialogues = BaseOrdersDialogues diff --git a/packages/valory/skills/strategy_evaluator_abci/fsm_specification.yaml b/packages/valory/skills/strategy_evaluator_abci/fsm_specification.yaml deleted file mode 100644 index 4576d28..0000000 --- a/packages/valory/skills/strategy_evaluator_abci/fsm_specification.yaml +++ /dev/null @@ -1,85 +0,0 @@ -alphabet_in: -- BACKTEST_FAILED -- BACKTEST_NEGATIVE -- BACKTEST_POSITIVE -- BACKTEST_POSITIVE_EVM -- BACKTEST_POSITIVE_PROXY_SERVER -- ERROR_BACKTESTING -- ERROR_PREPARING_INSTRUCTIONS -- ERROR_PREPARING_SWAPS -- INCOMPLETE_INSTRUCTIONS_PREPARED -- INSTRUCTIONS_PREPARED -- NO_INSTRUCTIONS -- NO_MAJORITY -- NO_ORDERS -- PREPARE_INCOMPLETE_SWAP -- PREPARE_SWAP -- PROXY_SWAPPED -- PROXY_SWAP_FAILED -- PROXY_SWAP_TIMEOUT -- ROUND_TIMEOUT -- SWAPS_QUEUE_EMPTY -- SWAP_TX_PREPARED -- TRANSACTION_PREPARED -- TX_PREPARATION_FAILED -default_start_state: StrategyExecRound -final_states: -- BacktestingFailedRound -- BacktestingNegativeRound -- HodlRound -- InstructionPreparationFailedRound -- NoMoreSwapsRound -- StrategyExecutionFailedRound -- SwapTxPreparedRound -label: StrategyEvaluatorAbciApp -start_states: -- StrategyExecRound -states: -- BacktestRound -- BacktestingFailedRound -- BacktestingNegativeRound -- HodlRound -- InstructionPreparationFailedRound -- NoMoreSwapsRound -- PrepareEvmSwapRound -- PrepareSwapRound -- ProxySwapQueueRound -- StrategyExecRound -- StrategyExecutionFailedRound -- SwapQueueRound -- SwapTxPreparedRound -transition_func: - (BacktestRound, BACKTEST_FAILED): BacktestingFailedRound - (BacktestRound, BACKTEST_NEGATIVE): BacktestingNegativeRound - (BacktestRound, BACKTEST_POSITIVE): PrepareSwapRound - (BacktestRound, BACKTEST_POSITIVE_EVM): PrepareEvmSwapRound - (BacktestRound, BACKTEST_POSITIVE_PROXY_SERVER): ProxySwapQueueRound - (BacktestRound, ERROR_BACKTESTING): BacktestingFailedRound - (BacktestRound, NO_MAJORITY): BacktestRound - (BacktestRound, ROUND_TIMEOUT): BacktestRound - (PrepareEvmSwapRound, ROUND_TIMEOUT): PrepareEvmSwapRound - (PrepareEvmSwapRound, NO_INSTRUCTIONS): PrepareEvmSwapRound - (PrepareEvmSwapRound, TRANSACTION_PREPARED): SwapTxPreparedRound - (PrepareEvmSwapRound, NO_MAJORITY): PrepareEvmSwapRound - (PrepareSwapRound, ERROR_PREPARING_INSTRUCTIONS): InstructionPreparationFailedRound - (PrepareSwapRound, INCOMPLETE_INSTRUCTIONS_PREPARED): SwapQueueRound - (PrepareSwapRound, INSTRUCTIONS_PREPARED): SwapQueueRound - (PrepareSwapRound, NO_INSTRUCTIONS): HodlRound - (PrepareSwapRound, NO_MAJORITY): PrepareSwapRound - (PrepareSwapRound, ROUND_TIMEOUT): PrepareSwapRound - (ProxySwapQueueRound, NO_MAJORITY): ProxySwapQueueRound - (ProxySwapQueueRound, PROXY_SWAPPED): ProxySwapQueueRound - (ProxySwapQueueRound, PROXY_SWAP_FAILED): ProxySwapQueueRound - (ProxySwapQueueRound, PROXY_SWAP_TIMEOUT): ProxySwapQueueRound - (ProxySwapQueueRound, SWAPS_QUEUE_EMPTY): NoMoreSwapsRound - (StrategyExecRound, ERROR_PREPARING_SWAPS): StrategyExecutionFailedRound - (StrategyExecRound, NO_MAJORITY): StrategyExecRound - (StrategyExecRound, NO_ORDERS): HodlRound - (StrategyExecRound, PREPARE_INCOMPLETE_SWAP): BacktestRound - (StrategyExecRound, PREPARE_SWAP): BacktestRound - (StrategyExecRound, ROUND_TIMEOUT): StrategyExecRound - (SwapQueueRound, NO_MAJORITY): SwapQueueRound - (SwapQueueRound, ROUND_TIMEOUT): SwapQueueRound - (SwapQueueRound, SWAPS_QUEUE_EMPTY): NoMoreSwapsRound - (SwapQueueRound, SWAP_TX_PREPARED): SwapTxPreparedRound - (SwapQueueRound, TX_PREPARATION_FAILED): SwapQueueRound diff --git a/packages/valory/skills/strategy_evaluator_abci/handlers.py b/packages/valory/skills/strategy_evaluator_abci/handlers.py deleted file mode 100644 index 5593cc5..0000000 --- a/packages/valory/skills/strategy_evaluator_abci/handlers.py +++ /dev/null @@ -1,67 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the handler for the 'strategy_evaluator_abci' skill.""" - -from packages.eightballer.protocols.orders.message import OrdersMessage -from packages.valory.skills.abstract_round_abci.handlers import ( - ABCIRoundHandler as BaseABCIRoundHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import AbstractResponseHandler -from packages.valory.skills.abstract_round_abci.handlers import ( - ContractApiHandler as BaseContractApiHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - HttpHandler as BaseHttpHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - IpfsHandler as BaseIpfsHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - LedgerApiHandler as BaseLedgerApiHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - SigningHandler as BaseSigningHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - TendermintHandler as BaseTendermintHandler, -) - - -class DcxtOrdersHandler(AbstractResponseHandler): - """This class implements a handler for DexTickersHandler messages.""" - - SUPPORTED_PROTOCOL = OrdersMessage.protocol_id - allowed_response_performatives = frozenset( - { - OrdersMessage.Performative.GET_ORDERS, - OrdersMessage.Performative.CREATE_ORDER, - OrdersMessage.Performative.ORDER_CREATED, - OrdersMessage.Performative.ERROR, - } - ) - - -ABCIHandler = BaseABCIRoundHandler -HttpHandler = BaseHttpHandler -SigningHandler = BaseSigningHandler -LedgerApiHandler = BaseLedgerApiHandler -ContractApiHandler = BaseContractApiHandler -TendermintHandler = BaseTendermintHandler -IpfsHandler = BaseIpfsHandler diff --git a/packages/valory/skills/strategy_evaluator_abci/models.py b/packages/valory/skills/strategy_evaluator_abci/models.py deleted file mode 100644 index 5a71132..0000000 --- a/packages/valory/skills/strategy_evaluator_abci/models.py +++ /dev/null @@ -1,148 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the models for the skill.""" - -from typing import Any, Dict, Iterable, List, Optional, Tuple, Type - -from aea.skills.base import SkillContext - -from packages.valory.skills.abstract_round_abci.base import AbciApp -from packages.valory.skills.abstract_round_abci.models import ApiSpecs, BaseParams -from packages.valory.skills.abstract_round_abci.models import ( - BenchmarkTool as BaseBenchmarkTool, -) -from packages.valory.skills.abstract_round_abci.models import Requests as BaseRequests -from packages.valory.skills.abstract_round_abci.models import ( - SharedState as BaseSharedState, -) -from packages.valory.skills.strategy_evaluator_abci.rounds import ( - StrategyEvaluatorAbciApp, -) - - -Requests = BaseRequests -BenchmarkTool = BaseBenchmarkTool - - -AMOUNT_PARAM = "amount" -SLIPPAGE_PARAM = "slippageBps" - - -class SharedState(BaseSharedState): - """Keep the current shared state of the skill.""" - - abci_app_cls: Type[AbciApp] = StrategyEvaluatorAbciApp - - def __init__(self, *args: Any, skill_context: SkillContext, **kwargs: Any) -> None: - """Initialize the state.""" - super().__init__(*args, skill_context=skill_context, **kwargs) - # utilized if using the proxy server - self.orders: Optional[List[Dict[str, str]]] = None - # utilized if using the Solana tx settlement - self.instructions: Optional[List[Dict[str, Any]]] = None - - def setup(self) -> None: - """Set up the model.""" - super().setup() - if ( - self.context.params.use_proxy_server - and self.synchronized_data.max_participants != 1 - ): - raise ValueError("Cannot use proxy server with a multi-agent service!") - - swap_apis: Tuple[ApiSpecs, ApiSpecs] = ( - self.context.swap_quotes, - self.context.tx_settlement_proxy, - ) - required_swap_params = (AMOUNT_PARAM, SLIPPAGE_PARAM) - for swap_api in swap_apis: - for swap_param in required_swap_params: - if swap_param not in swap_api.parameters: - exc = f"Api with id {swap_api.api_id!r} missing required parameter: {swap_param}!" - raise ValueError(exc) - - amounts = (api.parameters[AMOUNT_PARAM] for api in swap_apis) - expected_swap_tx_cost = self.context.params.expected_swap_tx_cost - if any(expected_swap_tx_cost > amount for amount in amounts): - exc = "The expected cost of the swap transaction cannot be greater than the swap amount!" - raise ValueError(exc) - - -def _raise_incorrect_config(key: str, values: Any) -> None: - """Raise a `ValueError` for incorrect configuration of a nested_list workaround.""" - raise ValueError( - f"The given configuration for {key!r} is incorrectly formatted: {values}!" - "The value is expected to be a list of lists that can be represented as a dictionary." - ) - - -def nested_list_todict_workaround( - kwargs: Dict, - key: str, -) -> Dict: - """Get a nested list from the kwargs and convert it to a dictionary.""" - values = list(kwargs.get(key, [])) - if len(values) == 0: - raise ValueError(f"No {key!r} specified in agent's configurations: {kwargs}!") - if any(not issubclass(type(nested_values), Iterable) for nested_values in values): - _raise_incorrect_config(key, values) - if any(len(nested_values) % 2 == 1 for nested_values in values): - _raise_incorrect_config(key, values) - return {value[0]: value[1] for value in values} - - -class StrategyEvaluatorParams(BaseParams): - """Strategy evaluator's parameters.""" - - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Initialize the parameters' object.""" - self.strategies_kwargs: Dict[str, List[Any]] = nested_list_todict_workaround( - kwargs, "strategies_kwargs" - ) - self.use_proxy_server: bool = self._ensure("use_proxy_server", kwargs, bool) - self.proxy_round_timeout_seconds: float = self._ensure( - "proxy_round_timeout_seconds", kwargs, float - ) - self.expected_swap_tx_cost: int = self._ensure( - "expected_swap_tx_cost", kwargs, int - ) - self.ipfs_fetch_retries: int = self._ensure("ipfs_fetch_retries", kwargs, int) - self.sharpe_threshold: float = self._ensure("sharpe_threshold", kwargs, float) - self.use_solana = self._ensure("use_solana", kwargs, bool) - self.base_tokens = self._ensure("base_tokens", kwargs, Dict[str, str]) - self.native_currencies = self._ensure( - "native_currencies", kwargs, Dict[str, str] - ) - self.trade_size_in_base_token = self._ensure( - "trade_size_in_base_token", kwargs, float - ) - super().__init__(*args, **kwargs) - - -class SwapQuotesSpecs(ApiSpecs): - """A model that wraps ApiSpecs for the Jupiter quotes specifications.""" - - -class SwapInstructionsSpecs(ApiSpecs): - """A model that wraps ApiSpecs for the Jupiter instructions specifications.""" - - -class TxSettlementProxy(ApiSpecs): - """A model that wraps ApiSpecs for the Solana transaction settlement proxy server.""" diff --git a/packages/valory/skills/strategy_evaluator_abci/payloads.py b/packages/valory/skills/strategy_evaluator_abci/payloads.py deleted file mode 100644 index e2cd549..0000000 --- a/packages/valory/skills/strategy_evaluator_abci/payloads.py +++ /dev/null @@ -1,55 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the transaction payloads for the strategy evaluator.""" - -from dataclasses import dataclass -from typing import Optional - -from packages.valory.skills.abstract_round_abci.base import BaseTxPayload - - -@dataclass(frozen=True) -class IPFSHashPayload(BaseTxPayload): - """Represents a transaction payload for an IPFS hash.""" - - ipfs_hash: Optional[str] - incomplete: Optional[bool] - - -@dataclass(frozen=True) -class SendSwapProxyPayload(BaseTxPayload): - """Represents a transaction payload for attempting a swap transaction via the proxy server.""" - - tx_id: Optional[str] - - -@dataclass(frozen=True) -class SendSwapPayload(BaseTxPayload): - """Represents a transaction payload for preparing the instruction for a swap transaction.""" - - # `instructions` is a serialized `List[Dict[str, Any]]` - instructions: Optional[str] - - -@dataclass(frozen=True) -class TransactionHashPayload(BaseTxPayload): - """Represent a transaction payload of type 'tx_hash'.""" - - tx_hash: Optional[str] diff --git a/packages/valory/skills/strategy_evaluator_abci/rounds.py b/packages/valory/skills/strategy_evaluator_abci/rounds.py deleted file mode 100644 index 5d2c057..0000000 --- a/packages/valory/skills/strategy_evaluator_abci/rounds.py +++ /dev/null @@ -1,208 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the rounds for the strategy evaluator.""" - -from typing import Dict, Set - -from packages.valory.skills.abstract_round_abci.base import ( - AbciApp, - AbciAppTransitionFunction, - AppState, - get_name, -) -from packages.valory.skills.strategy_evaluator_abci.states.backtesting import ( - BacktestRound, -) -from packages.valory.skills.strategy_evaluator_abci.states.base import ( - Event, - SynchronizedData, -) -from packages.valory.skills.strategy_evaluator_abci.states.final_states import ( - BacktestingFailedRound, - BacktestingNegativeRound, - HodlRound, - InstructionPreparationFailedRound, - NoMoreSwapsRound, - StrategyExecutionFailedRound, - SwapTxPreparedRound, -) -from packages.valory.skills.strategy_evaluator_abci.states.prepare_swap import ( - PrepareEvmSwapRound, - PrepareSwapRound, -) -from packages.valory.skills.strategy_evaluator_abci.states.proxy_swap_queue import ( - ProxySwapQueueRound, -) -from packages.valory.skills.strategy_evaluator_abci.states.strategy_exec import ( - StrategyExecRound, -) -from packages.valory.skills.strategy_evaluator_abci.states.swap_queue import ( - SwapQueueRound, -) - - -class StrategyEvaluatorAbciApp(AbciApp[Event]): - """StrategyEvaluatorAbciApp - - Initial round: StrategyExecRound - - Initial states: {StrategyExecRound} - - Transition states: - 0. StrategyExecRound - - prepare swap: 1. - - prepare incomplete swap: 1. - - no orders: 12. - - error preparing swaps: 8. - - no majority: 0. - - round timeout: 0. - 1. BacktestRound - - backtest succeeded: 2. - - prepare swap proxy server: 4. - - prepare swap evm: 5. - - backtest negative: 9. - - backtest failed: 10. - - error backtesting: 10. - - no majority: 1. - - round timeout: 1. - 2. PrepareSwapRound - - instructions prepared: 3. - - incomplete instructions prepared: 3. - - no instructions: 12. - - error preparing instructions: 11. - - no majority: 2. - - round timeout: 2. - 3. SwapQueueRound - - swap tx prepared: 6. - - swaps queue empty: 7. - - none: 3. - - no majority: 3. - - round timeout: 3. - 4. ProxySwapQueueRound - - proxy swapped: 4. - - swaps queue empty: 7. - - proxy swap failed: 4. - - no majority: 4. - - proxy swap timeout: 4. - 5. PrepareEvmSwapRound - - transaction prepared: 6. - - round timeout: 5. - - no instructions: 5. - - no majority: 5. - 6. SwapTxPreparedRound - 7. NoMoreSwapsRound - 8. StrategyExecutionFailedRound - 9. BacktestingNegativeRound - 10. BacktestingFailedRound - 11. InstructionPreparationFailedRound - 12. HodlRound - - Final states: {BacktestingFailedRound, BacktestingNegativeRound, HodlRound, InstructionPreparationFailedRound, NoMoreSwapsRound, StrategyExecutionFailedRound, SwapTxPreparedRound} - - Timeouts: - round timeout: 30.0 - proxy swap timeout: 1200.0 - """ - - initial_round_cls: AppState = StrategyExecRound - initial_states: Set[AppState] = {StrategyExecRound} - final_states: Set[AppState] = { - SwapTxPreparedRound, - NoMoreSwapsRound, - StrategyExecutionFailedRound, - InstructionPreparationFailedRound, - HodlRound, - BacktestingNegativeRound, - BacktestingFailedRound, - } - event_to_timeout: Dict[Event, float] = { - Event.ROUND_TIMEOUT: 30.0, - Event.PROXY_SWAP_TIMEOUT: 1200.0, - } - db_pre_conditions: Dict[AppState, Set[str]] = { - StrategyExecRound: { - get_name(SynchronizedData.selected_strategy), - get_name(SynchronizedData.data_hash), - }, - } - transition_function: AbciAppTransitionFunction = { - StrategyExecRound: { - Event.PREPARE_SWAP: BacktestRound, - Event.PREPARE_INCOMPLETE_SWAP: BacktestRound, - Event.NO_ORDERS: HodlRound, - Event.ERROR_PREPARING_SWAPS: StrategyExecutionFailedRound, - Event.NO_MAJORITY: StrategyExecRound, - Event.ROUND_TIMEOUT: StrategyExecRound, - }, - BacktestRound: { - Event.BACKTEST_POSITIVE: PrepareSwapRound, - Event.BACKTEST_POSITIVE_PROXY_SERVER: ProxySwapQueueRound, - Event.BACKTEST_POSITIVE_EVM: PrepareEvmSwapRound, - Event.BACKTEST_NEGATIVE: BacktestingNegativeRound, - Event.BACKTEST_FAILED: BacktestingFailedRound, - Event.ERROR_BACKTESTING: BacktestingFailedRound, - Event.NO_MAJORITY: BacktestRound, - Event.ROUND_TIMEOUT: BacktestRound, - }, - PrepareSwapRound: { - Event.INSTRUCTIONS_PREPARED: SwapQueueRound, - Event.INCOMPLETE_INSTRUCTIONS_PREPARED: SwapQueueRound, - Event.NO_INSTRUCTIONS: HodlRound, - Event.ERROR_PREPARING_INSTRUCTIONS: InstructionPreparationFailedRound, - Event.NO_MAJORITY: PrepareSwapRound, - Event.ROUND_TIMEOUT: PrepareSwapRound, - }, - SwapQueueRound: { - Event.SWAP_TX_PREPARED: SwapTxPreparedRound, - Event.SWAPS_QUEUE_EMPTY: NoMoreSwapsRound, - Event.TX_PREPARATION_FAILED: SwapQueueRound, - Event.NO_MAJORITY: SwapQueueRound, - Event.ROUND_TIMEOUT: SwapQueueRound, - }, - ProxySwapQueueRound: { - Event.PROXY_SWAPPED: ProxySwapQueueRound, - Event.SWAPS_QUEUE_EMPTY: NoMoreSwapsRound, - Event.PROXY_SWAP_FAILED: ProxySwapQueueRound, - Event.NO_MAJORITY: ProxySwapQueueRound, - Event.PROXY_SWAP_TIMEOUT: ProxySwapQueueRound, - }, - PrepareEvmSwapRound: { - Event.TRANSACTION_PREPARED: SwapTxPreparedRound, - Event.ROUND_TIMEOUT: PrepareEvmSwapRound, - Event.NO_INSTRUCTIONS: PrepareEvmSwapRound, - Event.NO_MAJORITY: PrepareEvmSwapRound, - }, - SwapTxPreparedRound: {}, - NoMoreSwapsRound: {}, - StrategyExecutionFailedRound: {}, - BacktestingNegativeRound: {}, - BacktestingFailedRound: {}, - InstructionPreparationFailedRound: {}, - HodlRound: {}, - } - db_post_conditions: Dict[AppState, Set[str]] = { - SwapTxPreparedRound: {get_name(SynchronizedData.most_voted_tx_hash)}, - NoMoreSwapsRound: set(), - StrategyExecutionFailedRound: set(), - BacktestingNegativeRound: set(), - BacktestingFailedRound: set(), - InstructionPreparationFailedRound: set(), - HodlRound: set(), - } diff --git a/packages/valory/skills/strategy_evaluator_abci/skill.yaml b/packages/valory/skills/strategy_evaluator_abci/skill.yaml deleted file mode 100644 index f77f468..0000000 --- a/packages/valory/skills/strategy_evaluator_abci/skill.yaml +++ /dev/null @@ -1,257 +0,0 @@ -name: strategy_evaluator_abci -author: valory -version: 0.1.0 -type: skill -description: This skill is responsible for the execution of the strategy and preparing - the swapping transaction. -license: Apache-2.0 -aea_version: '>=1.0.0, <2.0.0' -fingerprint: - README.md: bafybeicuikjki6yvgwmv6sxudjagjb3bhfsnypna36eceza2mzmqnthuiy - __init__.py: bafybeifhkyp44uusta77c4ths3xixmrtel3ssoq5mhcstcfsclkvirmw3a - behaviours/__init__.py: bafybeihpuluelzi5sbxgxbeks7rfsbcjivhj57cqoshpkxgufqec55s3rq - behaviours/backtesting.py: bafybeihquc6zopikmkzmc27627txajuzwqwcrnj65nnph55wbexk7f3k4u - behaviours/base.py: bafybeidzevmaz3kvnizxt6cdoozj7mrtnbw3pthck372y3xqngju4afvee - behaviours/prepare_swap_tx.py: bafybeibgvkxsssk65dqi44fwfzfpj66ud36jmpaxp7h2kvor37exqgu5jq - behaviours/proxy_swap_queue.py: bafybeihtsgtmh3sv4ptqw6msbuwhf3krgdrlzlu4gjrdqri55wmbxqk7gy - behaviours/round_behaviour.py: bafybeif26u7uapmhvtzdljj3hlvlqmdgb33dw3bif5lhbtpdjeghe7cdfy - behaviours/strategy_exec.py: bafybeiabstmctll3ofblf7lzso3tjkcptop2rgvf7nvsi34cg3rzggazy4 - behaviours/swap_queue.py: bafybeifuw22ri5vmso2krsspuahhbjzj4cm5v7bbbs2i6tqegw2aohvxby - dialogues.py: bafybeigxdc3i2wq5hp266op5lyyfswwbosphqzrcapgqxscrznggrllype - fsm_specification.yaml: bafybeifkf2mffocii4jspbjcx7wc5ji2mypksfil7ddgebiid2bi55pvfy - handlers.py: bafybeihwqu65rsc5lhixfnhxgocxcn7synhmw66cbzqrfmuevbtkvvwbae - models.py: bafybeictphbbro6hxbyf4waeg55vw5ytts3hwz4wxjuaructwuhu3ehpga - payloads.py: bafybeicpya7e2bedbgohbf4nbxiqi2jw2hrlp2keiwkcm6u6oxexvy2rsy - rounds.py: bafybeiegmi3xzdpvxld2ufkbrylzyl5g2xhjmxw3brw5ndgmrwo4cotdcy - states/__init__.py: bafybeidgoz7uxrcafbiq5mfg6tl7fcmxmd5auu3lexl7cxpbhlxfvfnoom - states/backtesting.py: bafybeid6hsayprjbkzfhfvkbw4azs3v3cf7u74vdi465ywmy2j5chfctnu - states/base.py: bafybeib2oej2sir7ufik4ukdgndq4o427yjmx6bmgqxoyiuhaarukxflti - states/final_states.py: bafybeigkrwvzg2o3exdiofxmunjn2kgolnnojiik7ckzi26dgn2ydh7mla - states/prepare_swap.py: bafybeihzb3muxzh7eedzkb45djizu5ayxxi3rovfc3rc4sqvyms5ufkcrm - states/proxy_swap_queue.py: bafybeidguarl5aoqlgc56df2v2xt6u7rsxij3aymeanfstjv2jsn3jtiay - states/strategy_exec.py: bafybeig72frgbgtxt3zccibv4ztnwvoydcfjmphipordf7gtf25xzk7ilq - states/swap_queue.py: bafybeihtnpowhqoym5lmpiob6wt5ejynnsfjg4j5usutohat4eyq54fzki -fingerprint_ignore_patterns: [] -connections: -- eightballer/dcxt:0.1.0:bafybeihes2id6qmqqybvdqfpchu6h3i5mpnhq6lztfxp52twd22iwqxacq -contracts: -- valory/gnosis_safe:0.1.0:bafybeiho6sbfts3zk3mftrngw37d5qnlvkqtnttt3fzexmcwkeevhu4wwi -protocols: -- eightballer/orders:0.1.0:bafybeibprhniaoq3y2uzc4arwwl7yws3i54ahaicrphh5gtl4xxhxqexdy -- valory/contract_api:1.0.0:bafybeidgu7o5llh26xp3u3ebq3yluull5lupiyeu6iooi2xyymdrgnzq5i -skills: -- valory/abstract_round_abci:0.1.0:bafybeibovsktd3uxur45nrcomq5shcn46cgxd5idmhxbmjhg32c5abyqim -- valory/market_data_fetcher_abci:0.1.0:bafybeia3kld7ogbaolbxskys7r5ccolhm53fqi4tdkrwnvilfm7gn5ztcm -- valory/trader_decision_maker_abci:0.1.0:bafybeih7duxvbjevmeez4bvdpahgvefoik3hnpjq3ik5g4jaqbuw5rybtu -- valory/portfolio_tracker_abci:0.1.0:bafybeigzyhm3fzoxhggjdexryzqgskafoi6rec4ois34n3asodxn6j3txm -- valory/transaction_settlement_abci:0.1.0:bafybeihq2yenstblmaadzcjousowj5kfn5l7ns5pxweq2gcrsczfyq5wzm -behaviours: - main: - args: {} - class_name: AgentStrategyEvaluatorRoundBehaviour -handlers: - abci: - args: {} - class_name: ABCIHandler - contract_api: - args: {} - class_name: ContractApiHandler - http: - args: {} - class_name: HttpHandler - ipfs: - args: {} - class_name: IpfsHandler - ledger_api: - args: {} - class_name: LedgerApiHandler - signing: - args: {} - class_name: SigningHandler - tendermint: - args: {} - class_name: TendermintHandler - orders: - args: {} - class_name: DcxtOrdersHandler -models: - abci_dialogues: - args: {} - class_name: AbciDialogues - benchmark_tool: - args: - log_dir: /logs - class_name: BenchmarkTool - contract_api_dialogues: - args: {} - class_name: ContractApiDialogues - http_dialogues: - args: {} - class_name: HttpDialogues - ipfs_dialogues: - args: {} - class_name: IpfsDialogues - ledger_api_dialogues: - args: {} - class_name: LedgerApiDialogues - params: - args: - cleanup_history_depth: 1 - cleanup_history_depth_current: null - drand_public_key: 868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31 - genesis_config: - genesis_time: '2022-05-20T16:00:21.735122717Z' - chain_id: chain-c4daS1 - consensus_params: - block: - max_bytes: '22020096' - max_gas: '-1' - time_iota_ms: '1000' - evidence: - max_age_num_blocks: '100000' - max_age_duration: '172800000000000' - max_bytes: '1048576' - validator: - pub_key_types: - - ed25519 - version: {} - voting_power: '10' - keeper_timeout: 30.0 - max_attempts: 10 - max_healthcheck: 120 - on_chain_service_id: null - request_retry_delay: 1.0 - request_timeout: 10.0 - reset_pause_duration: 10 - reset_tendermint_after: 2 - retry_attempts: 400 - retry_timeout: 3 - round_timeout_seconds: 350.0 - proxy_round_timeout_seconds: 1200.0 - service_id: decision_maker - service_registry_address: null - agent_registry_address: null - setup: - all_participants: - - '0x0000000000000000000000000000000000000000' - safe_contract_address: '0x0000000000000000000000000000000000000000' - consensus_threshold: null - share_tm_config_on_startup: false - sleep_time: 1 - use_slashing: false - slash_cooldown_hours: 3 - slash_threshold_amount: 10000000000000000 - light_slash_unit_amount: 5000000000000000 - serious_slash_unit_amount: 8000000000000000 - tendermint_check_sleep_delay: 3 - tendermint_com_url: http://localhost:8080 - tendermint_max_retries: 5 - tendermint_p2p_url: localhost:26656 - tendermint_url: http://localhost:26657 - tx_timeout: 10.0 - use_termination: false - strategies_kwargs: - - - extra_1 - - value - - - extra_2 - - value - use_proxy_server: false - use_solana: false - expected_swap_tx_cost: 20000000 - ipfs_fetch_retries: 5 - sharpe_threshold: 1.0 - base_tokens: - ethereum: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' - optimism: '0xda10009cbd5d07dd0cecc66161fc93d7c9000da1' - native_currencies: - ethereum: ETH - optimism: ETH - trade_size_in_base_token: 0.0001 - class_name: StrategyEvaluatorParams - swap_quotes: - args: - api_id: swap_quotes - headers: - Content-Type: application/json - method: GET - parameters: - amount: 100000000 - slippageBps: 5 - response_key: null - response_type: dict - retries: 5 - url: https://quote-api.jup.ag/v6/quote - class_name: SwapQuotesSpecs - swap_instructions: - args: - api_id: swap_instructions - headers: - Content-Type: application/json - method: POST - parameters: {} - response_key: null - response_type: dict - retries: 5 - url: https://quote-api.jup.ag/v6/swap-instructions - class_name: SwapInstructionsSpecs - tx_settlement_proxy: - args: - api_id: tx_settlement_proxy - headers: - Content-Type: application/json - method: POST - parameters: - amount: 100000000 - slippageBps: 5 - resendAmount: 200 - timeoutInMs: 120000 - priorityFee: 5000000 - response_key: null - response_type: dict - retries: 5 - url: http://tx_proxy:3000/tx - class_name: TxSettlementProxy - get_balance: - args: - api_id: get_balance - headers: - Content-Type: application/json - method: POST - parameters: {} - response_key: result:value - response_type: int - error_key: error:message - error_type: str - retries: 5 - url: replace_with_a_solana_rpc - class_name: GetBalance - token_accounts: - args: - api_id: token_accounts - headers: - Content-Type: application/json - method: POST - parameters: {} - response_key: result:value - response_type: list - error_key: error:message - error_type: str - retries: 5 - url: replace_with_a_solana_rpc - class_name: TokenAccounts - requests: - args: {} - class_name: Requests - signing_dialogues: - args: {} - class_name: SigningDialogues - state: - args: {} - class_name: SharedState - tendermint_dialogues: - args: {} - class_name: TendermintDialogues -dependencies: - pyyaml: - version: <=6.0.1,>=3.10 -is_abstract: true diff --git a/packages/valory/skills/strategy_evaluator_abci/states/__init__.py b/packages/valory/skills/strategy_evaluator_abci/states/__init__.py deleted file mode 100644 index ec05cda..0000000 --- a/packages/valory/skills/strategy_evaluator_abci/states/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This package contains the rounds for the 'strategy_evaluator_abci' skill.""" diff --git a/packages/valory/skills/strategy_evaluator_abci/states/backtesting.py b/packages/valory/skills/strategy_evaluator_abci/states/backtesting.py deleted file mode 100644 index c5b0e93..0000000 --- a/packages/valory/skills/strategy_evaluator_abci/states/backtesting.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the backtesting state of the swap(s).""" - -from typing import Any - -from packages.valory.skills.abstract_round_abci.base import get_name -from packages.valory.skills.strategy_evaluator_abci.states.base import ( - Event, - IPFSRound, - SynchronizedData, -) - - -class BacktestRound(IPFSRound): - """A round in which the agents prepare swap(s) instructions.""" - - done_event = Event.BACKTEST_POSITIVE - incomplete_event = Event.BACKTEST_FAILED - no_hash_event = Event.ERROR_BACKTESTING - none_event = Event.BACKTEST_NEGATIVE - selection_key = ( - get_name(SynchronizedData.backtested_orders_hash), - get_name(SynchronizedData.incomplete_exec), - ) - collection_key = get_name(SynchronizedData.participant_to_backtesting) - - def __init__(self, *args: Any, **kwargs: Any): - """Initialize the strategy execution round.""" - super().__init__(*args, **kwargs) - if self.context.params.use_proxy_server: - self.done_event = Event.BACKTEST_POSITIVE_PROXY_SERVER - # Note, using evm takes precedence over proxy server - if not self.context.params.use_solana: - self.done_event = Event.BACKTEST_POSITIVE_EVM diff --git a/packages/valory/skills/strategy_evaluator_abci/states/base.py b/packages/valory/skills/strategy_evaluator_abci/states/base.py deleted file mode 100644 index bc00b53..0000000 --- a/packages/valory/skills/strategy_evaluator_abci/states/base.py +++ /dev/null @@ -1,182 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the base functionality for the rounds of the decision-making abci app.""" - -from enum import Enum -from typing import Optional, Tuple, cast - -from packages.valory.skills.abstract_round_abci.base import ( - BaseSynchronizedData, - CollectSameUntilThresholdRound, - CollectionRound, - DeserializedCollection, -) -from packages.valory.skills.market_data_fetcher_abci.rounds import ( - SynchronizedData as MarketFetcherSyncedData, -) -from packages.valory.skills.portfolio_tracker_abci.rounds import ( - SynchronizedData as PortfolioTrackerSyncedData, -) -from packages.valory.skills.strategy_evaluator_abci.payloads import IPFSHashPayload -from packages.valory.skills.trader_decision_maker_abci.rounds import ( - SynchronizedData as DecisionMakerSyncedData, -) -from packages.valory.skills.transaction_settlement_abci.rounds import ( - SynchronizedData as TxSettlementSyncedData, -) - - -class Event(Enum): - """Event enumeration for the price estimation demo.""" - - NO_ORDERS = "no_orders" - PREPARE_SWAP = "prepare_swap" - BACKTEST_POSITIVE_PROXY_SERVER = "prepare_swap_proxy_server" - BACKTEST_POSITIVE_EVM = "prepare_swap_evm" - PREPARE_INCOMPLETE_SWAP = "prepare_incomplete_swap" - ERROR_PREPARING_SWAPS = "error_preparing_swaps" - NO_INSTRUCTIONS = "no_instructions" - INSTRUCTIONS_PREPARED = "instructions_prepared" - TRANSACTION_PREPARED = "transaction_prepared" - INCOMPLETE_INSTRUCTIONS_PREPARED = "incomplete_instructions_prepared" - ERROR_PREPARING_INSTRUCTIONS = "error_preparing_instructions" - SWAP_TX_PREPARED = "swap_tx_prepared" - SWAPS_QUEUE_EMPTY = "swaps_queue_empty" - TX_PREPARATION_FAILED = "none" - PROXY_SWAPPED = "proxy_swapped" - PROXY_SWAP_FAILED = "proxy_swap_failed" - BACKTEST_POSITIVE = "backtest_succeeded" - BACKTEST_NEGATIVE = "backtest_negative" - BACKTEST_FAILED = "backtest_failed" - ERROR_BACKTESTING = "error_backtesting" - ROUND_TIMEOUT = "round_timeout" - PROXY_SWAP_TIMEOUT = "proxy_swap_timeout" - NO_MAJORITY = "no_majority" - - -class SynchronizedData( - DecisionMakerSyncedData, - MarketFetcherSyncedData, - PortfolioTrackerSyncedData, - TxSettlementSyncedData, -): - """Class to represent the synchronized data. - - This data is replicated by the tendermint application. - """ - - def _optional_str(self, db_key: str) -> Optional[str]: - """Get an optional string from the db.""" - val = self.db.get_strict(db_key) - if val is None: - return None - return str(val) - - def _get_deserialized(self, key: str) -> DeserializedCollection: - """Strictly get a collection and return it deserialized.""" - serialized = self.db.get_strict(key) - return CollectionRound.deserialize_collection(serialized) - - @property - def orders_hash(self) -> Optional[str]: - """Get the hash of the orders' data.""" - return self._optional_str("orders_hash") - - @property - def backtested_orders_hash(self) -> Optional[str]: - """Get the hash of the backtested orders' data.""" - return self._optional_str("backtested_orders_hash") - - @property - def incomplete_exec(self) -> bool: - """Get whether the strategies did not complete successfully.""" - return bool(self.db.get_strict("incomplete_exec")) - - @property - def tx_id(self) -> str: - """Get the transaction's id.""" - return str(self.db.get_strict("tx_id")) - - @property - def instructions_hash(self) -> Optional[str]: - """Get the hash of the instructions' data.""" - return self._optional_str("instructions_hash") - - @property - def incomplete_instructions(self) -> bool: - """Get whether the instructions were not built for all the swaps.""" - return bool(self.db.get_strict("incomplete_instructions")) - - @property - def participant_to_orders(self) -> DeserializedCollection: - """Get the participants to orders.""" - return self._get_deserialized("participant_to_orders") - - @property - def participant_to_instructions(self) -> DeserializedCollection: - """Get the participants to swap(s) instructions.""" - return self._get_deserialized("participant_to_instructions") - - @property - def participant_to_tx_preparation(self) -> DeserializedCollection: - """Get the participants to the next swap's tx preparation.""" - return self._get_deserialized("participant_to_tx_preparation") - - @property - def participant_to_backtesting(self) -> DeserializedCollection: - """Get the participants to the backtesting.""" - return self._get_deserialized("participant_to_backtesting") - - -class IPFSRound(CollectSameUntilThresholdRound): - """A round for sending data to IPFS and storing the returned hash.""" - - payload_class = IPFSHashPayload - synchronized_data_class = SynchronizedData - incomplete_event: Event - no_hash_event: Event - no_majority_event = Event.NO_MAJORITY - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: - """Process the end of the block.""" - res = super().end_block() - if res is None: - return None - - synced_data, event = cast(Tuple[SynchronizedData, Enum], res) - if event == self.done_event: - return synced_data, self.get_swap_event(synced_data) - return synced_data, event - - def get_swap_event(self, synced_data: SynchronizedData) -> Enum: - """Get the swap event based on the synchronized data.""" - if not isinstance(self.selection_key, tuple) or len(self.selection_key) != 2: - raise ValueError( - f"The default implementation of `get_swap_event` for {self.__class__!r} " - "only supports two selection keys. " - "Please override the method to match the intended logic." - ) - - hash_db_key, incomplete_db_key = self.selection_key - if getattr(synced_data, hash_db_key) is None: - return self.no_hash_event - if getattr(synced_data, incomplete_db_key): - return self.incomplete_event - return self.done_event diff --git a/packages/valory/skills/strategy_evaluator_abci/states/final_states.py b/packages/valory/skills/strategy_evaluator_abci/states/final_states.py deleted file mode 100644 index dd34179..0000000 --- a/packages/valory/skills/strategy_evaluator_abci/states/final_states.py +++ /dev/null @@ -1,55 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the final states of the strategy evaluator abci app.""" - -from packages.valory.skills.abstract_round_abci.base import DegenerateRound - - -class SwapTxPreparedRound(DegenerateRound): - """A round representing that the strategy evaluator has prepared swap(s) transaction.""" - - -class NoMoreSwapsRound(DegenerateRound): - """A round representing that the strategy evaluator has no more swap transactions to prepare.""" - - -class HodlRound(DegenerateRound): - """A round representing that the strategy evaluator has not prepared any swap transactions.""" - - -class StrategyExecutionFailedRound(DegenerateRound): - """A round representing that the strategy evaluator has failed to execute the strategy.""" - - -class InstructionPreparationFailedRound(DegenerateRound): - """A round representing that the strategy evaluator has failed to prepare the instructions for the swaps.""" - - -class BacktestingNegativeRound(DegenerateRound): - """A round representing that the backtesting has returned with a negative result.""" - - -class BacktestingFailedRound(DegenerateRound): - """A round representing that the backtesting has failed to run.""" - - -# TODO use this in portfolio tracker -# class RefillRequiredRound(DegenerateRound): -# """A round representing that a refill is required for swapping.""" diff --git a/packages/valory/skills/strategy_evaluator_abci/states/prepare_swap.py b/packages/valory/skills/strategy_evaluator_abci/states/prepare_swap.py deleted file mode 100644 index 18cd6ff..0000000 --- a/packages/valory/skills/strategy_evaluator_abci/states/prepare_swap.py +++ /dev/null @@ -1,59 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the swap(s) instructions' preparation state of the strategy evaluator abci app.""" - -from packages.valory.skills.abstract_round_abci.base import ( - CollectSameUntilThresholdRound, - get_name, -) -from packages.valory.skills.strategy_evaluator_abci.payloads import ( - TransactionHashPayload, -) -from packages.valory.skills.strategy_evaluator_abci.states.base import ( - Event, - IPFSRound, - SynchronizedData, -) - - -class PrepareSwapRound(IPFSRound): - """A round in which the agents prepare swap(s) instructions.""" - - done_event = Event.INSTRUCTIONS_PREPARED - incomplete_event = Event.INCOMPLETE_INSTRUCTIONS_PREPARED - no_hash_event = Event.NO_INSTRUCTIONS - none_event = Event.ERROR_PREPARING_INSTRUCTIONS - selection_key = ( - get_name(SynchronizedData.instructions_hash), - get_name(SynchronizedData.incomplete_instructions), - ) - collection_key = get_name(SynchronizedData.participant_to_instructions) - - -class PrepareEvmSwapRound(CollectSameUntilThresholdRound): - """A round in which agents compute the transaction hash.""" - - payload_class = TransactionHashPayload - synchronized_data_class = SynchronizedData - done_event = Event.TRANSACTION_PREPARED - none_event = Event.NO_INSTRUCTIONS - no_majority_event = Event.NO_MAJORITY - collection_key = get_name(SynchronizedData.participant_to_signature) - selection_key = get_name(SynchronizedData.most_voted_tx_hash) diff --git a/packages/valory/skills/strategy_evaluator_abci/states/proxy_swap_queue.py b/packages/valory/skills/strategy_evaluator_abci/states/proxy_swap_queue.py deleted file mode 100644 index f7c049b..0000000 --- a/packages/valory/skills/strategy_evaluator_abci/states/proxy_swap_queue.py +++ /dev/null @@ -1,57 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the state for preparing a transaction for the next swap in the queue of instructions.""" - -from enum import Enum -from typing import Optional, Tuple, cast - -from packages.valory.skills.abstract_round_abci.base import ( - BaseSynchronizedData, - CollectSameUntilThresholdRound, - get_name, -) -from packages.valory.skills.strategy_evaluator_abci.payloads import SendSwapProxyPayload -from packages.valory.skills.strategy_evaluator_abci.states.base import ( - Event, - SynchronizedData, -) - - -class ProxySwapQueueRound(CollectSameUntilThresholdRound): - """A round in which one agent utilizes the proxy server to perform the next swap transaction in priority.""" - - payload_class = SendSwapProxyPayload - synchronized_data_class = SynchronizedData - done_event = Event.PROXY_SWAPPED - none_event = Event.PROXY_SWAP_FAILED - no_majority_event = Event.NO_MAJORITY - selection_key = get_name(SynchronizedData.tx_id) - collection_key = get_name(SynchronizedData.participant_to_tx_preparation) - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: - """Process the end of the block.""" - res = super().end_block() - if res is None: - return None - - synced_data, event = cast(Tuple[SynchronizedData, Enum], res) - if event == self.done_event and synced_data.tx_id == "": - return synced_data, Event.SWAPS_QUEUE_EMPTY - return synced_data, event diff --git a/packages/valory/skills/strategy_evaluator_abci/states/strategy_exec.py b/packages/valory/skills/strategy_evaluator_abci/states/strategy_exec.py deleted file mode 100644 index ea8abd9..0000000 --- a/packages/valory/skills/strategy_evaluator_abci/states/strategy_exec.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the strategy execution state of the strategy evaluator abci app.""" - -from packages.valory.skills.abstract_round_abci.base import get_name -from packages.valory.skills.strategy_evaluator_abci.states.base import ( - Event, - IPFSRound, - SynchronizedData, -) - - -class StrategyExecRound(IPFSRound): - """A round for executing a strategy.""" - - done_event = Event.PREPARE_SWAP - incomplete_event = Event.PREPARE_INCOMPLETE_SWAP - no_hash_event = Event.NO_ORDERS - none_event = Event.ERROR_PREPARING_SWAPS - selection_key = ( - get_name(SynchronizedData.orders_hash), - get_name(SynchronizedData.incomplete_exec), - ) - collection_key = get_name(SynchronizedData.participant_to_orders) diff --git a/packages/valory/skills/strategy_evaluator_abci/states/swap_queue.py b/packages/valory/skills/strategy_evaluator_abci/states/swap_queue.py deleted file mode 100644 index 9657eb0..0000000 --- a/packages/valory/skills/strategy_evaluator_abci/states/swap_queue.py +++ /dev/null @@ -1,59 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the state for preparing a transaction for the next swap in the queue of instructions.""" - -from enum import Enum -from typing import Optional, Tuple, cast - -from packages.valory.skills.abstract_round_abci.base import ( - BaseSynchronizedData, - CollectSameUntilThresholdRound, - get_name, -) -from packages.valory.skills.strategy_evaluator_abci.payloads import SendSwapPayload -from packages.valory.skills.strategy_evaluator_abci.states.base import ( - Event, - SynchronizedData, -) - - -class SwapQueueRound(CollectSameUntilThresholdRound): - """A round in which the agents prepare a swap transaction.""" - - payload_class = SendSwapPayload - synchronized_data_class = SynchronizedData - done_event = Event.SWAP_TX_PREPARED - none_event = Event.TX_PREPARATION_FAILED - no_majority_event = Event.NO_MAJORITY - # TODO replace `most_voted_randomness` with `most_voted_instruction_set` when solana tx settlement is ready - selection_key = get_name(SynchronizedData.most_voted_randomness) - collection_key = get_name(SynchronizedData.participant_to_tx_preparation) - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: - """Process the end of the block.""" - res = super().end_block() - if res is None: - return None - - synced_data, event = cast(Tuple[SynchronizedData, Enum], res) - # TODO replace `most_voted_randomness` with `most_voted_instruction_set` when solana tx settlement is ready - if event == self.done_event and synced_data.most_voted_randomness == "": - return synced_data, Event.SWAPS_QUEUE_EMPTY - return synced_data, event diff --git a/packages/valory/skills/trader_decision_maker_abci/README.md b/packages/valory/skills/trader_decision_maker_abci/README.md deleted file mode 100644 index 28bb394..0000000 --- a/packages/valory/skills/trader_decision_maker_abci/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# TraderDecisionMakerAbci - -## Description - -This module contains the 'trader_decision_maker_abci' skill for an AEA. diff --git a/packages/valory/skills/trader_decision_maker_abci/__init__.py b/packages/valory/skills/trader_decision_maker_abci/__init__.py deleted file mode 100644 index 1808a23..0000000 --- a/packages/valory/skills/trader_decision_maker_abci/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains a strategy selection skill based on a greedy policy.""" - -from aea.configurations.base import PublicId - - -PUBLIC_ID = PublicId.from_str("valory/trader_decision_maker_abci:0.1.0") diff --git a/packages/valory/skills/trader_decision_maker_abci/behaviours.py b/packages/valory/skills/trader_decision_maker_abci/behaviours.py deleted file mode 100644 index e5aa454..0000000 --- a/packages/valory/skills/trader_decision_maker_abci/behaviours.py +++ /dev/null @@ -1,234 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the behaviours for the 'trader_decision_maker_abci' skill.""" - -import json -from abc import ABC -from pathlib import Path -from typing import ( - Any, - Callable, - Generator, - List, - Optional, - Set, - Tuple, - Type, - TypeVar, - cast, -) - -from packages.valory.skills.abstract_round_abci.base import AbstractRound -from packages.valory.skills.abstract_round_abci.behaviour_utils import BaseBehaviour -from packages.valory.skills.abstract_round_abci.behaviours import AbstractRoundBehaviour -from packages.valory.skills.abstract_round_abci.common import ( - RandomnessBehaviour as RandomnessBehaviourBase, -) -from packages.valory.skills.trader_decision_maker_abci.models import Params -from packages.valory.skills.trader_decision_maker_abci.payloads import ( - RandomnessPayload, - TraderDecisionMakerPayload, -) -from packages.valory.skills.trader_decision_maker_abci.policy import EGreedyPolicy -from packages.valory.skills.trader_decision_maker_abci.rounds import ( - Position, - RandomnessRound, - SynchronizedData, - TraderDecisionMakerAbciApp, - TraderDecisionMakerRound, -) - - -DeserializedType = TypeVar("DeserializedType") - - -POLICY_STORE = "policy_store.json" -POSITIONS_STORE = "positions.json" -STRATEGIES_STORE = "strategies.json" - - -class RandomnessBehaviour(RandomnessBehaviourBase): - """Retrieve randomness.""" - - matching_round = RandomnessRound - payload_class = RandomnessPayload - - -class TraderDecisionMakerBehaviour(BaseBehaviour, ABC): - """A behaviour in which the agents select a trading strategy.""" - - matching_round: Type[AbstractRound] = TraderDecisionMakerRound - - def __init__(self, **kwargs: Any) -> None: - """Initialize Behaviour.""" - super().__init__(**kwargs) - base_dir = Path(self.context.data_dir) - self.policy_path = base_dir / POLICY_STORE - self.positions_path = base_dir / POSITIONS_STORE - self.strategies_path = base_dir / STRATEGIES_STORE - self.strategies: Tuple[str, ...] = tuple(self.context.shared_state.keys()) - - @property - def params(self) -> Params: - """Get the parameters.""" - return cast(Params, self.context.params) - - @property - def synchronized_data(self) -> SynchronizedData: - """Return the synchronized data.""" - return SynchronizedData(super().synchronized_data.db) - - @property - def policy(self) -> EGreedyPolicy: - """Get the policy.""" - if self._policy is None: - raise ValueError( - "Attempting to retrieve the policy before it has been established." - ) - return self._policy - - @property - def is_first_period(self) -> bool: - """Return whether it is the first period of the service.""" - return self.synchronized_data.period_count == 0 - - @property - def positions(self) -> List[Position]: - """Get the positions of the service.""" - if self.is_first_period: - positions = self._try_recover_from_store( - self.positions_path, - Position.from_json, - ) - if positions is not None: - return positions - return [] - return self.synchronized_data.positions - - def _adjust_policy_strategies(self, local: List[str]) -> None: - """Add or remove strategies from the locally stored policy to match the strategies given via the config.""" - # remove strategies if they are not available anymore - # process the indices in a reverse order to avoid index shifting when removing the unavailable strategies later - reversed_idx = range(len(local) - 1, -1, -1) - removed_idx = [idx for idx in reversed_idx if local[idx] not in self.strategies] - self.policy.remove_strategies(removed_idx) - - # add strategies if there are new ones available - # process the indices in a reverse order to avoid index shifting when adding the new strategies later - reversed_idx = range(len(self.strategies) - 1, -1, -1) - new_idx = [idx for idx in reversed_idx if self.strategies[idx] not in local] - self.policy.add_new_strategies(new_idx) - - def _set_policy(self) -> None: - """Set the E Greedy Policy.""" - if not self.is_first_period: - self._policy = self.synchronized_data.policy - return - - self._policy = self._get_init_policy() - local_strategies = self._try_recover_from_store(self.strategies_path) - if local_strategies is not None: - self._adjust_policy_strategies(local_strategies) - - def _get_init_policy(self) -> EGreedyPolicy: - """Get the initial policy""" - # try to read the policy from the policy store - policy = self._try_recover_from_store( - self.policy_path, lambda policy_: EGreedyPolicy(**policy_) - ) - if policy is not None: - # we successfully recovered the policy, so we return it - return policy - - # we could not recover the policy, so we create a new one - n_relevant = len(self.strategies) - policy = EGreedyPolicy.initial_state(self.params.epsilon, n_relevant) - return policy - - def _try_recover_from_store( - self, - path: Path, - deserializer: Optional[Callable[[Any], DeserializedType]] = None, - ) -> Optional[DeserializedType]: - """Try to recover a previously saved file from the policy store.""" - try: - with open(path, "r") as stream: - res = json.load(stream) - if deserializer is None: - return res - return deserializer(res) - except Exception as e: - self.context.logger.warning( - f"Could not recover file from the policy store: {e}." - ) - return None - - def select_strategy(self) -> Optional[str]: - """Select a strategy based on an e-greedy policy and return its index.""" - self._set_policy() - randomness = self.synchronized_data.most_voted_randomness - selected_idx = self.policy.select_strategy(randomness) - selected = self.strategies[selected_idx] if selected_idx is not None else "NaN" - self.context.logger.info(f"Selected strategy: {selected}.") - return selected - - def _store_policy(self) -> None: - """Store the policy.""" - with open(self.policy_path, "w") as policy_stream: - policy_stream.write(self.policy.serialize()) - - def _store_available_strategies(self) -> None: - """Store the available strategies.""" - with open(self.strategies_path, "w") as strategies_stream: - json.dump(self.strategies, strategies_stream) - - def async_act(self) -> Generator: - """Do the action.""" - with self.context.benchmark_tool.measure(self.behaviour_id).local(): - policy = positions = None - selected_strategy = self.select_strategy() - if selected_strategy is not None: - policy = self.policy.serialize() - positions = json.dumps(self.positions, sort_keys=True) - self._store_policy() - self._store_available_strategies() - - payload = TraderDecisionMakerPayload( - self.context.agent_address, - policy, - positions, - selected_strategy, - ) - - with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): - yield from self.send_a2a_transaction(payload) - yield from self.wait_until_round_end() - self.set_done() - - -class TraderDecisionMakerRoundBehaviour(AbstractRoundBehaviour): - """This behaviour manages the consensus stages for the TraderDecisionMakerBehaviour.""" - - initial_behaviour_cls = RandomnessBehaviour - abci_app_cls = TraderDecisionMakerAbciApp - behaviours: Set[Type[BaseBehaviour]] = { - RandomnessBehaviour, # type: ignore - TraderDecisionMakerBehaviour, # type: ignore - } diff --git a/packages/valory/skills/trader_decision_maker_abci/dialogues.py b/packages/valory/skills/trader_decision_maker_abci/dialogues.py deleted file mode 100644 index 661d747..0000000 --- a/packages/valory/skills/trader_decision_maker_abci/dialogues.py +++ /dev/null @@ -1,90 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the classes required for dialogue management.""" - -from packages.valory.skills.abstract_round_abci.dialogues import ( - AbciDialogue as BaseAbciDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - AbciDialogues as BaseAbciDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - ContractApiDialogue as BaseContractApiDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - ContractApiDialogues as BaseContractApiDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - HttpDialogue as BaseHttpDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - HttpDialogues as BaseHttpDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - IpfsDialogue as BaseIpfsDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - IpfsDialogues as BaseIpfsDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - LedgerApiDialogue as BaseLedgerApiDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - LedgerApiDialogues as BaseLedgerApiDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - SigningDialogue as BaseSigningDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - SigningDialogues as BaseSigningDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - TendermintDialogue as BaseTendermintDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - TendermintDialogues as BaseTendermintDialogues, -) - - -AbciDialogue = BaseAbciDialogue -AbciDialogues = BaseAbciDialogues - - -HttpDialogue = BaseHttpDialogue -HttpDialogues = BaseHttpDialogues - - -SigningDialogue = BaseSigningDialogue -SigningDialogues = BaseSigningDialogues - - -LedgerApiDialogue = BaseLedgerApiDialogue -LedgerApiDialogues = BaseLedgerApiDialogues - - -ContractApiDialogue = BaseContractApiDialogue -ContractApiDialogues = BaseContractApiDialogues - -TendermintDialogue = BaseTendermintDialogue -TendermintDialogues = BaseTendermintDialogues - - -IpfsDialogue = BaseIpfsDialogue -IpfsDialogues = BaseIpfsDialogues diff --git a/packages/valory/skills/trader_decision_maker_abci/fsm_specification.yaml b/packages/valory/skills/trader_decision_maker_abci/fsm_specification.yaml deleted file mode 100644 index ff06e9a..0000000 --- a/packages/valory/skills/trader_decision_maker_abci/fsm_specification.yaml +++ /dev/null @@ -1,25 +0,0 @@ -alphabet_in: -- DONE -- NONE -- NO_MAJORITY -- ROUND_TIMEOUT -default_start_state: RandomnessRound -final_states: -- FailedTraderDecisionMakerRound -- FinishedTraderDecisionMakerRound -label: TraderDecisionMakerAbciApp -start_states: -- RandomnessRound -states: -- FailedTraderDecisionMakerRound -- FinishedTraderDecisionMakerRound -- RandomnessRound -- TraderDecisionMakerRound -transition_func: - (RandomnessRound, DONE): TraderDecisionMakerRound - (RandomnessRound, NO_MAJORITY): RandomnessRound - (RandomnessRound, ROUND_TIMEOUT): RandomnessRound - (TraderDecisionMakerRound, DONE): FinishedTraderDecisionMakerRound - (TraderDecisionMakerRound, NONE): FailedTraderDecisionMakerRound - (TraderDecisionMakerRound, NO_MAJORITY): FailedTraderDecisionMakerRound - (TraderDecisionMakerRound, ROUND_TIMEOUT): FailedTraderDecisionMakerRound diff --git a/packages/valory/skills/trader_decision_maker_abci/handlers.py b/packages/valory/skills/trader_decision_maker_abci/handlers.py deleted file mode 100644 index 2b18af0..0000000 --- a/packages/valory/skills/trader_decision_maker_abci/handlers.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - - -"""This module contains the handlers for the 'trader_decision_maker_abci' skill.""" - -from packages.valory.skills.abstract_round_abci.handlers import ABCIRoundHandler -from packages.valory.skills.abstract_round_abci.handlers import ( - ContractApiHandler as BaseContractApiHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - HttpHandler as BaseHttpHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - IpfsHandler as BaseIpfsHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - LedgerApiHandler as BaseLedgerApiHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - SigningHandler as BaseSigningHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - TendermintHandler as BaseTendermintHandler, -) - - -ABCITraderDecisionMakerHandler = ABCIRoundHandler -HttpHandler = BaseHttpHandler -SigningHandler = BaseSigningHandler -LedgerApiHandler = BaseLedgerApiHandler -ContractApiHandler = BaseContractApiHandler -TendermintHandler = BaseTendermintHandler -IpfsHandler = BaseIpfsHandler diff --git a/packages/valory/skills/trader_decision_maker_abci/models.py b/packages/valory/skills/trader_decision_maker_abci/models.py deleted file mode 100644 index 811a336..0000000 --- a/packages/valory/skills/trader_decision_maker_abci/models.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - - -"""Custom objects for the 'trader_decision_maker_abci' skill.""" - -from typing import Any - -from packages.valory.skills.abstract_round_abci.models import BaseParams -from packages.valory.skills.abstract_round_abci.models import ( - BenchmarkTool as BaseBenchmarkTool, -) -from packages.valory.skills.abstract_round_abci.models import Requests as BaseRequests -from packages.valory.skills.abstract_round_abci.models import ( - SharedState as BaseSharedState, -) -from packages.valory.skills.trader_decision_maker_abci.rounds import ( - TraderDecisionMakerAbciApp, -) - - -Requests = BaseRequests -BenchmarkTool = BaseBenchmarkTool - - -class SharedState(BaseSharedState): - """Keep the current shared state of the skill.""" - - abci_app_cls = TraderDecisionMakerAbciApp - - -class Params(BaseParams): - """Market manager's parameters.""" - - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Initialize the parameters' object.""" - self.epsilon: float = self._ensure("epsilon", kwargs, float) - super().__init__(*args, **kwargs) diff --git a/packages/valory/skills/trader_decision_maker_abci/payloads.py b/packages/valory/skills/trader_decision_maker_abci/payloads.py deleted file mode 100644 index 499d308..0000000 --- a/packages/valory/skills/trader_decision_maker_abci/payloads.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the transaction payloads for the 'trader_decision_maker_abci' skill.""" - -from dataclasses import dataclass -from typing import Optional - -from packages.valory.skills.abstract_round_abci.base import BaseTxPayload - - -@dataclass(frozen=True) -class RandomnessPayload(BaseTxPayload): - """Represent a transaction payload carrying randomness data.""" - - round_id: int - randomness: str - - -@dataclass(frozen=True) -class TraderDecisionMakerPayload(BaseTxPayload): - """A transaction payload for the TraderDecisionMakingRound.""" - - policy: Optional[str] - positions: Optional[str] - selected_strategy: Optional[str] diff --git a/packages/valory/skills/trader_decision_maker_abci/policy.py b/packages/valory/skills/trader_decision_maker_abci/policy.py deleted file mode 100644 index 361031e..0000000 --- a/packages/valory/skills/trader_decision_maker_abci/policy.py +++ /dev/null @@ -1,129 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2023-2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains an Epsilon Greedy Policy implementation.""" - -import json -import random -from dataclasses import dataclass -from typing import List, Optional, Union - -from packages.valory.skills.trader_decision_maker_abci.utils import DataclassEncoder - - -RandomnessType = Union[int, float, str, bytes, bytearray, None] - - -def argmax(li: List) -> int: - """Get the index of the max value within the provided list.""" - return li.index((max(li))) - - -@dataclass -class EGreedyPolicy: - """An e-Greedy policy for the strategy selection.""" - - eps: float - counts: List[int] - rewards: List[float] - initial_value = 0 - - @classmethod - def initial_state(cls, eps: float, n_strategies: int) -> "EGreedyPolicy": - """Return an instance on its initial state.""" - if n_strategies <= 0 or eps > 1 or eps < 0: - raise ValueError( - f"Cannot initialize an e Greedy Policy with {eps=} and {n_strategies=}" - ) - - return EGreedyPolicy( - eps, - [cls.initial_value] * n_strategies, - [float(cls.initial_value)] * n_strategies, - ) - - @property - def n_strategies(self) -> int: - """Get the number of the policy's strategies.""" - return len(self.counts) - - @property - def random_strategy(self) -> int: - """Get the index of a strategy randomly.""" - return random.randrange(self.n_strategies) # nosec - - @property - def has_updated(self) -> bool: - """Whether the policy has ever been updated since its genesis or not.""" - return sum(self.counts) > 0 - - @property - def reward_rates(self) -> List[float]: - """Get the reward rates.""" - return [ - reward / count if count > 0 else 0 - for reward, count in zip(self.rewards, self.counts) - ] - - @property - def best_strategy(self) -> int: - """Get the best strategy.""" - return argmax(self.reward_rates) - - def add_new_strategies(self, indexes: List[int], avoid_shift: bool = False) -> None: - """Add new strategies to the current policy.""" - if avoid_shift: - indexes = sorted(indexes, reverse=True) - - for i in indexes: - self.counts.insert(i, self.initial_value) - self.rewards.insert(i, float(self.initial_value)) - - def remove_strategies(self, indexes: List[int], avoid_shift: bool = False) -> None: - """Remove the knowledge for the strategies corresponding to the given indexes.""" - if avoid_shift: - indexes = sorted(indexes, reverse=True) - - for i in indexes: - try: - del self.counts[i] - del self.rewards[i] - except IndexError as exc: - error = "Attempted to remove strategies using incorrect indexes!" - raise ValueError(error) from exc - - def select_strategy(self, randomness: RandomnessType) -> Optional[int]: - """Select a strategy and return its index.""" - if self.n_strategies == 0: - return None - - random.seed(randomness) - if sum(self.reward_rates) == 0 or random.random() < self.eps: # nosec - return self.random_strategy - - return self.best_strategy - - def add_reward(self, index: int, reward: float = 0) -> None: - """Add a reward for the strategy corresponding to the given index.""" - self.counts[index] += 1 - self.rewards[index] += reward - - def serialize(self) -> str: - """Return the policy serialized.""" - return json.dumps(self, cls=DataclassEncoder, sort_keys=True) diff --git a/packages/valory/skills/trader_decision_maker_abci/rounds.py b/packages/valory/skills/trader_decision_maker_abci/rounds.py deleted file mode 100644 index c5bc79e..0000000 --- a/packages/valory/skills/trader_decision_maker_abci/rounds.py +++ /dev/null @@ -1,232 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the rounds for the 'trader_decision_maker_abci' skill.""" - -import json -from abc import ABC -from dataclasses import dataclass -from enum import Enum -from typing import Dict, List, Set, Tuple, Type, cast - -from packages.valory.skills.abstract_round_abci.base import ( - AbciApp, - AbciAppTransitionFunction, - AbstractRound, - AppState, - BaseSynchronizedData, - CollectSameUntilThresholdRound, - CollectionRound, - DegenerateRound, - DeserializedCollection, - get_name, -) -from packages.valory.skills.trader_decision_maker_abci.payloads import ( - RandomnessPayload, - TraderDecisionMakerPayload, -) -from packages.valory.skills.trader_decision_maker_abci.policy import EGreedyPolicy - - -class Event(Enum): - """Event enumeration for the TraderDecisionMakerAbci demo.""" - - DONE = "done" - NONE = "none" - NO_MAJORITY = "no_majority" - ROUND_TIMEOUT = "round_timeout" - - -@dataclass -class Position: - """A swap position.""" - - from_token: str - to_token: str - amount: int - - @classmethod - def from_json(cls, positions: List[Dict]) -> List["Position"]: - """Return a list of positions from a JSON representation.""" - return [cls(**position) for position in positions] - - -class SynchronizedData(BaseSynchronizedData): - """Class to represent the synchronized data. - - This data is replicated by the tendermint application. - """ - - def _get_deserialized(self, key: str) -> DeserializedCollection: - """Strictly get a collection and return it deserialized.""" - serialized = self.db.get_strict(key) - return CollectionRound.deserialize_collection(serialized) - - @property - def participant_to_decision(self) -> DeserializedCollection: - """Get the participants to decision.""" - return self._get_deserialized("participant_to_decision") - - @property - def most_voted_randomness_round(self) -> int: - """Get the most voted randomness round.""" - round_ = self.db.get_strict("most_voted_randomness_round") - return int(round_) - - @property - def selected_strategy(self) -> str: - """Get the selected strategy.""" - return str(self.db.get_strict("selected_strategy")) - - @property - def policy(self) -> EGreedyPolicy: - """Get the policy.""" - policy = self.db.get_strict("policy") - return EGreedyPolicy(**json.loads(policy)) - - @property - def positions(self) -> List[Position]: - """Get the swap positions.""" - positions = json.loads(self.db.get_strict("positions")) - return Position.from_json(positions) - - -class TraderDecisionMakerAbstractRound(AbstractRound[Event], ABC): - """Abstract round for the TraderDecisionMakerAbci skill.""" - - @property - def synchronized_data(self) -> SynchronizedData: - """Return the synchronized data.""" - return cast(SynchronizedData, super().synchronized_data) - - def _return_no_majority_event(self) -> Tuple[SynchronizedData, Event]: - """ - Trigger the `NO_MAJORITY` event. - - :return: the new synchronized data and a `NO_MAJORITY` event - """ - return self.synchronized_data, Event.NO_MAJORITY - - -class RandomnessRound(CollectSameUntilThresholdRound): - """A round for generating randomness.""" - - payload_class = RandomnessPayload - synchronized_data_class = SynchronizedData - done_event = Event.DONE - no_majority_event = Event.NO_MAJORITY - collection_key = get_name(SynchronizedData.participant_to_randomness) - selection_key = ( - get_name(SynchronizedData.most_voted_randomness_round), - get_name(SynchronizedData.most_voted_randomness), - ) - - -class TraderDecisionMakerRound( - CollectSameUntilThresholdRound, TraderDecisionMakerAbstractRound -): - """A round for the bets fetching & updating.""" - - payload_class = TraderDecisionMakerPayload - done_event: Enum = Event.DONE - none_event: Enum = Event.NONE - no_majority_event: Enum = Event.NO_MAJORITY - selection_key = ( - get_name(SynchronizedData.policy), - get_name(SynchronizedData.positions), - get_name(SynchronizedData.selected_strategy), - ) - collection_key = get_name(SynchronizedData.participant_to_decision) - synchronized_data_class = SynchronizedData - - -class FinishedTraderDecisionMakerRound(DegenerateRound, ABC): - """A round that represents that the ABCI app has finished""" - - -class FailedTraderDecisionMakerRound(DegenerateRound, ABC): - """A round that represents that the ABCI app has failed""" - - -class TraderDecisionMakerAbciApp(AbciApp[Event]): - """TraderDecisionMakerAbciApp - - Initial round: RandomnessRound - - Initial states: {RandomnessRound} - - Transition states: - 0. RandomnessRound - - done: 1. - - round timeout: 0. - - no majority: 0. - 1. TraderDecisionMakerRound - - done: 2. - - none: 3. - - round timeout: 3. - - no majority: 3. - 2. FinishedTraderDecisionMakerRound - 3. FailedTraderDecisionMakerRound - - Final states: {FailedTraderDecisionMakerRound, FinishedTraderDecisionMakerRound} - - Timeouts: - round timeout: 30.0 - """ - - initial_round_cls: Type[AbstractRound] = RandomnessRound - transition_function: AbciAppTransitionFunction = { - RandomnessRound: { - Event.DONE: TraderDecisionMakerRound, - Event.ROUND_TIMEOUT: RandomnessRound, - Event.NO_MAJORITY: RandomnessRound, - }, - TraderDecisionMakerRound: { - Event.DONE: FinishedTraderDecisionMakerRound, - Event.NONE: FailedTraderDecisionMakerRound, - Event.ROUND_TIMEOUT: FailedTraderDecisionMakerRound, - Event.NO_MAJORITY: FailedTraderDecisionMakerRound, - }, - FinishedTraderDecisionMakerRound: {}, - FailedTraderDecisionMakerRound: {}, - } - final_states: Set[AppState] = { - FinishedTraderDecisionMakerRound, - FailedTraderDecisionMakerRound, - } - event_to_timeout: Dict[Event, float] = { - Event.ROUND_TIMEOUT: 30.0, - } - cross_period_persisted_keys = frozenset( - {get_name(SynchronizedData.policy), get_name(SynchronizedData.positions)} - ) - db_pre_conditions: Dict[AppState, Set[str]] = {RandomnessRound: set()} - db_post_conditions: Dict[AppState, Set[str]] = { - FinishedTraderDecisionMakerRound: { - get_name(SynchronizedData.selected_strategy), - get_name(SynchronizedData.policy), - get_name(SynchronizedData.positions), - get_name(SynchronizedData.most_voted_randomness_round), - get_name(SynchronizedData.most_voted_randomness), - }, - FailedTraderDecisionMakerRound: { - get_name(SynchronizedData.most_voted_randomness_round), - get_name(SynchronizedData.most_voted_randomness), - }, - } diff --git a/packages/valory/skills/trader_decision_maker_abci/skill.yaml b/packages/valory/skills/trader_decision_maker_abci/skill.yaml deleted file mode 100644 index 0dc7ace..0000000 --- a/packages/valory/skills/trader_decision_maker_abci/skill.yaml +++ /dev/null @@ -1,142 +0,0 @@ -name: trader_decision_maker_abci -author: valory -version: 0.1.0 -type: skill -description: This skill implements the TraderDecisionMakerAbci for an AEA. -license: Apache-2.0 -aea_version: '>=1.0.0, <2.0.0' -fingerprint: - README.md: bafybeih6idgiwf3bbqhikxeldldnhqtrkyi2gf4lfctrkivdy5pno6ilte - __init__.py: bafybeie5vho3br34gvl6x2lqfnudpjjeqgcfguvxl3p7l4kiyfxe5e323u - behaviours.py: bafybeih2i54x7jikjdxfdekpfylwcs4fjiz46ylynlzathpt5jl2xz6p4a - dialogues.py: bafybeieyljhi2bfvzytt6kxstbec7g5h5cauaul76guloysriqm22yo5fm - fsm_specification.yaml: bafybeihlwtlx3k2dcbjmahesfppgfrkruwwi75lcji6meqeobyaxjnydqu - handlers.py: bafybeifke4t4gg6fqb3zy3c6t4yginstypknvfgvj5u67yibhxlmo2cguy - models.py: bafybeifkzeulrztqnyekqrugcwchy4os4yz75rkxnxgmnmjnxp3qiiyxny - payloads.py: bafybeicwjheax7c4wpjcaxysbhwxoxv3z4gxldbcilv4gbhwshxahmjkru - policy.py: bafybeie5xilj3gv3yo3licei4r7hlfli65n4curxe3savtoj6unzksngtq - rounds.py: bafybeid3isvqi7oune2qrz2vj55ydew6ifaf5pnktp4m7xhy6ib73luo54 - utils.py: bafybeiblton2rvdxlv3bnu3asubtfwt2tk77lwrxw3hmesv3qarh3ew6ke -fingerprint_ignore_patterns: [] -connections: [] -contracts: [] -protocols: [] -skills: -- valory/abstract_round_abci:0.1.0:bafybeibovsktd3uxur45nrcomq5shcn46cgxd5idmhxbmjhg32c5abyqim -behaviours: - main: - args: {} - class_name: TraderDecisionMakerBehaviour -handlers: - abci: - args: {} - class_name: ABCITraderDecisionMakerHandler - contract_api: - args: {} - class_name: ContractApiHandler - http: - args: {} - class_name: HttpHandler - ipfs: - args: {} - class_name: IpfsHandler - ledger_api: - args: {} - class_name: LedgerApiHandler - signing: - args: {} - class_name: SigningHandler - tendermint: - args: {} - class_name: TendermintHandler -models: - abci_dialogues: - args: {} - class_name: AbciDialogues - benchmark_tool: - args: - log_dir: /logs - class_name: BenchmarkTool - contract_api_dialogues: - args: {} - class_name: ContractApiDialogues - http_dialogues: - args: {} - class_name: HttpDialogues - ipfs_dialogues: - args: {} - class_name: IpfsDialogues - ledger_api_dialogues: - args: {} - class_name: LedgerApiDialogues - params: - args: - cleanup_history_depth: 1 - cleanup_history_depth_current: null - drand_public_key: 868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31 - genesis_config: - genesis_time: '2022-05-20T16:00:21.735122717Z' - chain_id: chain-c4daS1 - consensus_params: - block: - max_bytes: '22020096' - max_gas: '-1' - time_iota_ms: '1000' - evidence: - max_age_num_blocks: '100000' - max_age_duration: '172800000000000' - max_bytes: '1048576' - validator: - pub_key_types: - - ed25519 - version: {} - voting_power: '10' - keeper_timeout: 30.0 - max_attempts: 10 - max_healthcheck: 120 - multisend_address: '0x0000000000000000000000000000000000000000' - on_chain_service_id: null - request_retry_delay: 1.0 - request_timeout: 10.0 - reset_pause_duration: 10 - reset_tendermint_after: 2 - retry_attempts: 400 - retry_timeout: 3 - round_timeout_seconds: 350.0 - service_id: market_manager - service_registry_address: null - setup: - all_participants: - - '0x0000000000000000000000000000000000000000' - safe_contract_address: '0x0000000000000000000000000000000000000000' - consensus_threshold: null - share_tm_config_on_startup: false - sleep_time: 5 - tendermint_check_sleep_delay: 3 - tendermint_com_url: http://localhost:8080 - tendermint_max_retries: 5 - tendermint_p2p_url: localhost:26656 - tendermint_url: http://localhost:26657 - termination_sleep: 900 - tx_timeout: 10.0 - use_termination: false - use_slashing: false - slash_cooldown_hours: 3 - slash_threshold_amount: 10000000000000000 - light_slash_unit_amount: 5000000000000000 - serious_slash_unit_amount: 8000000000000000 - epsilon: 0.1 - class_name: TraderDecisionMakerParams - requests: - args: {} - class_name: Requests - signing_dialogues: - args: {} - class_name: SigningDialogues - state: - args: {} - class_name: SharedState -dependencies: - web3: - version: <7,>=6.0.0 -is_abstract: true diff --git a/packages/valory/skills/trader_decision_maker_abci/utils.py b/packages/valory/skills/trader_decision_maker_abci/utils.py deleted file mode 100644 index 446d464..0000000 --- a/packages/valory/skills/trader_decision_maker_abci/utils.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains general purpose utilities.""" - -import json -from dataclasses import asdict, is_dataclass -from typing import Any - - -class DataclassEncoder(json.JSONEncoder): - """A custom JSON encoder for dataclasses.""" - - def default(self, o: Any) -> Any: - """The default JSON encoder.""" - if is_dataclass(o): - return asdict(o) - return super().default(o) diff --git a/packages/valory/skills/transaction_settlement_abci/README.md b/packages/valory/skills/transaction_settlement_abci/README.md deleted file mode 100644 index 29124a9..0000000 --- a/packages/valory/skills/transaction_settlement_abci/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# Transaction settlement abci - -## Description - -This module contains the ABCI transaction settlement skill for an AEA. - -## Behaviours - -* `BaseResetBehaviour` - - Reset state. - -* `FinalizeBehaviour` - - Finalize state. - -* `RandomnessTransactionSubmissionBehaviour` - - Retrieve randomness. - -* `ResetAndPauseBehaviour` - - Reset and pause state. - -* `ResetBehaviour` - - Reset state. - -* `SelectKeeperTransactionSubmissionBehaviourA` - - Select the keeper agent. - -* `SelectKeeperTransactionSubmissionBehaviourB` - - Select the keeper agent. - -* `SignatureBehaviour` - - Signature state. - -* `TransactionSettlementBaseState` - - Base state behaviour for the common apps' skill. - -* `ValidateTransactionBehaviour` - - Validate a transaction. - - -## Handlers - -No Handlers (the skill is purely behavioural). - diff --git a/packages/valory/skills/transaction_settlement_abci/__init__.py b/packages/valory/skills/transaction_settlement_abci/__init__.py deleted file mode 100644 index 1df75f1..0000000 --- a/packages/valory/skills/transaction_settlement_abci/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the ABCI transaction settlement skill for an AEA.""" - -from aea.configurations.base import PublicId - - -PUBLIC_ID = PublicId.from_str("valory/transaction_settlement_abci:0.1.0") diff --git a/packages/valory/skills/transaction_settlement_abci/behaviours.py b/packages/valory/skills/transaction_settlement_abci/behaviours.py deleted file mode 100644 index d3a7ca2..0000000 --- a/packages/valory/skills/transaction_settlement_abci/behaviours.py +++ /dev/null @@ -1,984 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the behaviours for the 'abci' skill.""" - -import binascii -import pprint -import re -from abc import ABC -from collections import deque -from typing import ( - Any, - Deque, - Dict, - Generator, - Iterator, - List, - Optional, - Set, - Tuple, - Type, - Union, - cast, -) - -from aea.protocols.base import Message -from web3.types import Nonce, TxData, Wei - -from packages.valory.contracts.gnosis_safe.contract import GnosisSafeContract -from packages.valory.protocols.contract_api.message import ContractApiMessage -from packages.valory.skills.abstract_round_abci.behaviour_utils import RPCResponseStatus -from packages.valory.skills.abstract_round_abci.behaviours import ( - AbstractRoundBehaviour, - BaseBehaviour, -) -from packages.valory.skills.abstract_round_abci.common import ( - RandomnessBehaviour, - SelectKeeperBehaviour, -) -from packages.valory.skills.abstract_round_abci.utils import VerifyDrand -from packages.valory.skills.transaction_settlement_abci.models import TransactionParams -from packages.valory.skills.transaction_settlement_abci.payload_tools import ( - VerificationStatus, - skill_input_hex_to_payload, - tx_hist_payload_to_hex, -) -from packages.valory.skills.transaction_settlement_abci.payloads import ( - CheckTransactionHistoryPayload, - FinalizationTxPayload, - RandomnessPayload, - ResetPayload, - SelectKeeperPayload, - SignaturePayload, - SynchronizeLateMessagesPayload, - ValidatePayload, -) -from packages.valory.skills.transaction_settlement_abci.rounds import ( - CheckLateTxHashesRound, - CheckTransactionHistoryRound, - CollectSignatureRound, - FinalizationRound, - RandomnessTransactionSubmissionRound, - ResetRound, - SelectKeeperTransactionSubmissionARound, - SelectKeeperTransactionSubmissionBAfterTimeoutRound, - SelectKeeperTransactionSubmissionBRound, - SynchronizeLateMessagesRound, - SynchronizedData, - TransactionSubmissionAbciApp, - ValidateTransactionRound, -) - - -TxDataType = Dict[str, Union[VerificationStatus, Deque[str], int, Set[str], str]] - -drand_check = VerifyDrand() - -REVERT_CODE_RE = r"\s(GS\d{3})[^\d]" - -# This mapping was copied from: -# https://github.com/safe-global/safe-contracts/blob/ce5cbd256bf7a8a34538c7e5f1f2366a9d685f34/docs/error_codes.md -REVERT_CODES_TO_REASONS: Dict[str, str] = { - "GS000": "Could not finish initialization", - "GS001": "Threshold needs to be defined", - "GS010": "Not enough gas to execute Safe transaction", - "GS011": "Could not pay gas costs with ether", - "GS012": "Could not pay gas costs with token", - "GS013": "Safe transaction failed when gasPrice and safeTxGas were 0", - "GS020": "Signatures data too short", - "GS021": "Invalid contract signature location: inside static part", - "GS022": "Invalid contract signature location: length not present", - "GS023": "Invalid contract signature location: data not complete", - "GS024": "Invalid contract signature provided", - "GS025": "Hash has not been approved", - "GS026": "Invalid owner provided", - "GS030": "Only owners can approve a hash", - "GS031": "Method can only be called from this contract", - "GS100": "Modules have already been initialized", - "GS101": "Invalid module address provided", - "GS102": "Module has already been added", - "GS103": "Invalid prevModule, module pair provided", - "GS104": "Method can only be called from an enabled module", - "GS200": "Owners have already been setup", - "GS201": "Threshold cannot exceed owner count", - "GS202": "Threshold needs to be greater than 0", - "GS203": "Invalid owner address provided", - "GS204": "Address is already an owner", - "GS205": "Invalid prevOwner, owner pair provided", - "GS300": "Guard does not implement IERC165", -} - - -class TransactionSettlementBaseBehaviour(BaseBehaviour, ABC): - """Base behaviour for the common apps' skill.""" - - @property - def synchronized_data(self) -> SynchronizedData: - """Return the synchronized data.""" - return cast(SynchronizedData, super().synchronized_data) - - @property - def params(self) -> TransactionParams: - """Return the params.""" - return cast(TransactionParams, super().params) - - @staticmethod - def serialized_keepers(keepers: Deque[str], keeper_retries: int) -> str: - """Get the keepers serialized.""" - if len(keepers) == 0: - return "" - keepers_ = "".join(keepers) - keeper_retries_ = keeper_retries.to_bytes(32, "big").hex() - concatenated = keeper_retries_ + keepers_ - - return concatenated - - def get_gas_price_params(self, tx_body: dict) -> List[str]: - """Guess the gas strategy from the transaction params""" - strategy_to_params: Dict[str, List[str]] = { - "eip": ["maxPriorityFeePerGas", "maxFeePerGas"], - "gas_station": ["gasPrice"], - } - - for strategy, params in strategy_to_params.items(): - if all(param in tx_body for param in params): - self.context.logger.info(f"Detected gas strategy: {strategy}") - return params - - return [] - - def _get_tx_data( - self, - message: ContractApiMessage, - use_flashbots: bool, - manual_gas_limit: int = 0, - raise_on_failed_simulation: bool = False, - chain_id: Optional[str] = None, - ) -> Generator[None, None, TxDataType]: - """Get the transaction data from a `ContractApiMessage`.""" - tx_data: TxDataType = { - "status": VerificationStatus.PENDING, - "keepers": self.synchronized_data.keepers, - "keeper_retries": self.synchronized_data.keeper_retries, - "blacklisted_keepers": self.synchronized_data.blacklisted_keepers, - "tx_digest": "", - } - - # Check for errors in the transaction preparation - if ( - message.performative == ContractApiMessage.Performative.ERROR - and message.message is not None - ): - if self._safe_nonce_reused(message.message): - tx_data["status"] = VerificationStatus.VERIFIED - else: - tx_data["status"] = VerificationStatus.ERROR - self.context.logger.warning(self._parse_revert_reason(message)) - return tx_data - - # Check that we have a RAW_TRANSACTION response - if message.performative != ContractApiMessage.Performative.RAW_TRANSACTION: - self.context.logger.warning( - f"get_raw_safe_transaction unsuccessful! Received: {message}" - ) - return tx_data - - if manual_gas_limit > 0: - message.raw_transaction.body["gas"] = manual_gas_limit - - # Send transaction - tx_digest, rpc_status = yield from self.send_raw_transaction( - message.raw_transaction, - use_flashbots, - raise_on_failed_simulation=raise_on_failed_simulation, - chain_id=chain_id, - ) - - # Handle transaction results - if rpc_status == RPCResponseStatus.ALREADY_KNOWN: - self.context.logger.warning( - "send_raw_transaction unsuccessful! Transaction is already in the mempool! Will attempt to verify it." - ) - - if rpc_status == RPCResponseStatus.INCORRECT_NONCE: - tx_data["status"] = VerificationStatus.ERROR - self.context.logger.warning( - "send_raw_transaction unsuccessful! Incorrect nonce." - ) - - if rpc_status == RPCResponseStatus.INSUFFICIENT_FUNDS: - # blacklist self. - tx_data["status"] = VerificationStatus.INSUFFICIENT_FUNDS - blacklisted = cast(Deque[str], tx_data["keepers"]).popleft() - tx_data["keeper_retries"] = 1 - cast(Set[str], tx_data["blacklisted_keepers"]).add(blacklisted) - self.context.logger.warning( - "send_raw_transaction unsuccessful! Insufficient funds." - ) - - if rpc_status not in { - RPCResponseStatus.SUCCESS, - RPCResponseStatus.ALREADY_KNOWN, - }: - self.context.logger.warning( - f"send_raw_transaction unsuccessful! Received: {rpc_status}" - ) - return tx_data - - tx_data["tx_digest"] = cast(str, tx_digest) - - nonce = Nonce(int(cast(str, message.raw_transaction.body["nonce"]))) - fallback_gas = message.raw_transaction.body["gas"] - - # Get the gas params - gas_price_params = self.get_gas_price_params(message.raw_transaction.body) - - gas_price = { - gas_price_param: Wei( - int( - cast( - str, - message.raw_transaction.body[gas_price_param], - ) - ) - ) - for gas_price_param in gas_price_params - } - - # Set hash, nonce and tip. - self.params.mutable_params.tx_hash = cast(str, tx_data["tx_digest"]) - if nonce == self.params.mutable_params.nonce: - self.context.logger.info( - "Attempting to replace transaction " - f"with old gas price parameters {self.params.mutable_params.gas_price}, using new gas price parameters {gas_price}" - ) - else: - self.context.logger.info( - f"Sent transaction for mining with gas parameters {gas_price}" - ) - self.params.mutable_params.nonce = nonce - self.params.mutable_params.gas_price = gas_price - self.params.mutable_params.fallback_gas = fallback_gas - - return tx_data - - def _verify_tx(self, tx_hash: str) -> Generator[None, None, ContractApiMessage]: - """Verify a transaction.""" - tx_params = skill_input_hex_to_payload( - self.synchronized_data.most_voted_tx_hash - ) - chain_id = self.synchronized_data.get_chain_id(self.params.default_chain_id) - contract_api_msg = yield from self.get_contract_api_response( - performative=ContractApiMessage.Performative.GET_STATE, # type: ignore - contract_address=self.synchronized_data.safe_contract_address, - contract_id=str(GnosisSafeContract.contract_id), - contract_callable="verify_tx", - tx_hash=tx_hash, - owners=tuple(self.synchronized_data.participants), - to_address=tx_params["to_address"], - value=tx_params["ether_value"], - data=tx_params["data"], - safe_tx_gas=tx_params["safe_tx_gas"], - signatures_by_owner={ - key: payload.signature - for key, payload in self.synchronized_data.participant_to_signature.items() - }, - operation=tx_params["operation"], - chain_id=chain_id, - ) - return contract_api_msg - - @staticmethod - def _safe_nonce_reused(revert_reason: str) -> bool: - """Check for GS026.""" - return "GS026" in revert_reason - - @staticmethod - def _parse_revert_reason(message: ContractApiMessage) -> str: - """Parse a revert reason and log a relevant message.""" - default_message = f"get_raw_safe_transaction unsuccessful! Received: {message}" - - revert_reason = message.message - if not revert_reason: - return default_message - - revert_match = re.findall(REVERT_CODE_RE, revert_reason) - if revert_match is None or len(revert_match) != 1: - return default_message - - revert_code = revert_match.pop() - revert_explanation = REVERT_CODES_TO_REASONS.get(revert_code, None) - if revert_explanation is None: - return default_message - - return f"Received a {revert_code} revert error: {revert_explanation}." - - def _get_safe_nonce(self) -> Generator[None, None, ContractApiMessage]: - """Get the safe nonce.""" - chain_id = self.synchronized_data.get_chain_id(self.params.default_chain_id) - contract_api_msg = yield from self.get_contract_api_response( - performative=ContractApiMessage.Performative.GET_STATE, # type: ignore - contract_address=self.synchronized_data.safe_contract_address, - contract_id=str(GnosisSafeContract.contract_id), - contract_callable="get_safe_nonce", - chain_id=chain_id, - ) - return contract_api_msg - - -class RandomnessTransactionSubmissionBehaviour(RandomnessBehaviour): - """Retrieve randomness.""" - - matching_round = RandomnessTransactionSubmissionRound - payload_class = RandomnessPayload - - -class SelectKeeperTransactionSubmissionBehaviourA( # pylint: disable=too-many-ancestors - SelectKeeperBehaviour, TransactionSettlementBaseBehaviour -): - """Select the keeper agent.""" - - matching_round = SelectKeeperTransactionSubmissionARound - payload_class = SelectKeeperPayload - - def async_act(self) -> Generator: - """Do the action.""" - - with self.context.benchmark_tool.measure(self.behaviour_id).local(): - keepers = deque((self._select_keeper(),)) - payload = self.payload_class( - self.context.agent_address, self.serialized_keepers(keepers, 1) - ) - - with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): - yield from self.send_a2a_transaction(payload) - yield from self.wait_until_round_end() - - self.set_done() - - -class SelectKeeperTransactionSubmissionBehaviourB( # pylint: disable=too-many-ancestors - SelectKeeperTransactionSubmissionBehaviourA -): - """Select the keeper b agent.""" - - matching_round = SelectKeeperTransactionSubmissionBRound - - def async_act(self) -> Generator: - """ - Do the action. - - Steps: - - If we have not selected enough keepers for the period, - select a keeper randomly and add it to the keepers' queue, with top priority. - - Otherwise, cycle through the keepers' subset, using the following logic: - A `PENDING` verification status means that we have not received any errors, - therefore, all we know is that the tx has not been mined yet due to low pricing. - Consequently, we are going to retry with the same keeper in order to replace the transaction. - However, if we receive a status other than `PENDING`, we need to cycle through the keepers' subset. - Moreover, if the current keeper has reached the allowed number of retries, then we cycle anyway. - - Send the transaction with the keepers and wait for it to be mined. - - Wait until ABCI application transitions to the next round. - - Go to the next behaviour (set done event). - """ - - with self.context.benchmark_tool.measure(self.behaviour_id).local(): - keepers = self.synchronized_data.keepers - keeper_retries = 1 - - if self.synchronized_data.keepers_threshold_exceeded: - keepers.rotate(-1) - self.context.logger.info(f"Rotated keepers to: {keepers}.") - elif ( - self.synchronized_data.keeper_retries - != self.params.keeper_allowed_retries - and self.synchronized_data.final_verification_status - == VerificationStatus.PENDING - ): - keeper_retries += self.synchronized_data.keeper_retries - self.context.logger.info( - f"Kept keepers and incremented retries: {keepers}." - ) - else: - keepers.appendleft(self._select_keeper()) - - payload = self.payload_class( - self.context.agent_address, - self.serialized_keepers(keepers, keeper_retries), - ) - - with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): - yield from self.send_a2a_transaction(payload) - yield from self.wait_until_round_end() - - self.set_done() - - -class SelectKeeperTransactionSubmissionBehaviourBAfterTimeout( # pylint: disable=too-many-ancestors - SelectKeeperTransactionSubmissionBehaviourB -): - """Select the keeper b agent after a timeout.""" - - matching_round = SelectKeeperTransactionSubmissionBAfterTimeoutRound - - -class ValidateTransactionBehaviour(TransactionSettlementBaseBehaviour): - """Validate a transaction.""" - - matching_round = ValidateTransactionRound - - def async_act(self) -> Generator: - """ - Do the action. - - Steps: - - Validate that the transaction hash provided by the keeper points to a - valid transaction. - - Send the transaction with the validation result and wait for it to be - mined. - - Wait until ABCI application transitions to the next round. - - Go to the next behaviour (set done event). - """ - - with self.context.benchmark_tool.measure(self.behaviour_id).local(): - is_correct = yield from self.has_transaction_been_sent() - if is_correct: - self.context.logger.info( - f"Finalized with transaction hash: {self.synchronized_data.to_be_validated_tx_hash}" - ) - payload = ValidatePayload(self.context.agent_address, is_correct) - - with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): - yield from self.send_a2a_transaction(payload) - yield from self.wait_until_round_end() - - self.set_done() - - def has_transaction_been_sent(self) -> Generator[None, None, Optional[bool]]: - """Transaction verification.""" - - to_be_validated_tx_hash = self.synchronized_data.to_be_validated_tx_hash - - response = yield from self.get_transaction_receipt( - to_be_validated_tx_hash, - self.params.retry_timeout, - self.params.retry_attempts, - chain_id=self.synchronized_data.get_chain_id(self.params.default_chain_id), - ) - if response is None: # pragma: nocover - self.context.logger.error( - f"tx {to_be_validated_tx_hash} receipt check timed out!" - ) - return None - - contract_api_msg = yield from self._verify_tx(to_be_validated_tx_hash) - if ( - contract_api_msg.performative != ContractApiMessage.Performative.STATE - ): # pragma: nocover - self.context.logger.error( - f"verify_tx unsuccessful! Received: {contract_api_msg}" - ) - return False - - verified = cast(bool, contract_api_msg.state.body["verified"]) - verified_log = ( - f"Verified result: {verified}" - if verified - else f"Verified result: {verified}, all: {contract_api_msg.state.body}" - ) - self.context.logger.info(verified_log) - return verified - - -class CheckTransactionHistoryBehaviour(TransactionSettlementBaseBehaviour): - """Check the transaction history.""" - - matching_round = CheckTransactionHistoryRound - check_expected_to_be_verified = "The next tx check" - - @property - def history(self) -> List[str]: - """Get the history of hashes.""" - return self.synchronized_data.tx_hashes_history - - def async_act(self) -> Generator: - """Do the action.""" - - with self.context.benchmark_tool.measure(self.behaviour_id).local(): - verification_status, tx_hash = yield from self._check_tx_history() - if verification_status == VerificationStatus.VERIFIED: - msg = f"A previous transaction {tx_hash} has already been verified " - msg += ( - f"for {self.synchronized_data.to_be_validated_tx_hash}." - if self.synchronized_data.tx_hashes_history - else "and was synced after the finalization round timed out." - ) - self.context.logger.info(msg) - elif verification_status == VerificationStatus.NOT_VERIFIED: - self.context.logger.info( - f"No previous transaction has been verified for " - f"{self.synchronized_data.to_be_validated_tx_hash}." - ) - - verified_res = tx_hist_payload_to_hex(verification_status, tx_hash) - payload = CheckTransactionHistoryPayload( - self.context.agent_address, verified_res - ) - - with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): - yield from self.send_a2a_transaction(payload) - yield from self.wait_until_round_end() - - self.set_done() - - def _check_tx_history( # pylint: disable=too-many-return-statements - self, - ) -> Generator[None, None, Tuple[VerificationStatus, Optional[str]]]: - """Check the transaction history.""" - if not self.history: - self.context.logger.error( - "An unexpected error occurred! The synchronized data do not contain any transaction hashes, " - f"but entered the `{self.behaviour_id}` behaviour." - ) - return VerificationStatus.ERROR, None - - contract_api_msg = yield from self._get_safe_nonce() - if ( - contract_api_msg.performative != ContractApiMessage.Performative.STATE - ): # pragma: nocover - self.context.logger.error( - f"get_safe_nonce unsuccessful! Received: {contract_api_msg}" - ) - return VerificationStatus.ERROR, None - - safe_nonce = cast(int, contract_api_msg.state.body["safe_nonce"]) - if safe_nonce == self.params.mutable_params.nonce: - # if we have reached this state it means that the transaction didn't go through in the expected time - # as such we assume it is not verified - self.context.logger.info( - f"Safe nonce is the same as the nonce used in the transaction: {safe_nonce}. " - f"No transaction has gone through yet." - ) - return VerificationStatus.NOT_VERIFIED, None - - self.context.logger.info( - f"A transaction with nonce {safe_nonce} has already been sent. " - ) - self.context.logger.info( - f"Starting check for the transaction history: {self.history}. " - ) - was_nonce_reused = False - for tx_hash in self.history[::-1]: - self.context.logger.info(f"Checking hash {tx_hash}...") - contract_api_msg = yield from self._verify_tx(tx_hash) - - if ( - contract_api_msg.performative != ContractApiMessage.Performative.STATE - ): # pragma: nocover - self.context.logger.error( - f"verify_tx unsuccessful for {tx_hash}! Received: {contract_api_msg}" - ) - return VerificationStatus.ERROR, tx_hash - - verified = cast(bool, contract_api_msg.state.body["verified"]) - verified_log = f"Verified result for {tx_hash}: {verified}" - - if verified: - self.context.logger.info(verified_log) - return VerificationStatus.VERIFIED, tx_hash - - self.context.logger.info( - verified_log + f", all: {contract_api_msg.state.body}" - ) - - status = cast(int, contract_api_msg.state.body["status"]) - if status == -1: - self.context.logger.info(f"Tx hash {tx_hash} has no receipt!") - # this loop might take a long time - # we do not want to starve the rest of the behaviour - # we yield which freezes this loop here until the - # AbstractRoundBehaviour it belongs to, sends a tick to it - yield - continue - - tx_data = cast(TxData, contract_api_msg.state.body["transaction"]) - revert_reason = yield from self._get_revert_reason(tx_data) - - if revert_reason is not None: - if self._safe_nonce_reused(revert_reason): - self.context.logger.info( - f"The safe's nonce has been reused for {tx_hash}. " - f"{self.check_expected_to_be_verified} is expected to be verified!" - ) - was_nonce_reused = True - # this loop might take a long time - # we do not want to starve the rest of the behaviour - # we yield which freezes this loop here until the - # AbstractRoundBehaviour it belongs to, sends a tick to it - yield - continue - - self.context.logger.warning( - f"Payload is invalid for {tx_hash}! Cannot continue. Received: {revert_reason}" - ) - - return VerificationStatus.INVALID_PAYLOAD, tx_hash - - if was_nonce_reused: - self.context.logger.info( - f"Safe nonce {safe_nonce} was used, but no valid transaction was found. " - f"We cannot resend the transaction with the same nonce." - ) - return VerificationStatus.BAD_SAFE_NONCE, None - - return VerificationStatus.NOT_VERIFIED, None - - def _get_revert_reason(self, tx: TxData) -> Generator[None, None, Optional[str]]: - """Get the revert reason of the given transaction.""" - chain_id = self.synchronized_data.get_chain_id(self.params.default_chain_id) - contract_api_msg = yield from self.get_contract_api_response( - performative=ContractApiMessage.Performative.GET_STATE, # type: ignore - contract_address=self.synchronized_data.safe_contract_address, - contract_id=str(GnosisSafeContract.contract_id), - contract_callable="revert_reason", - tx=tx, - chain_id=chain_id, - ) - - if ( - contract_api_msg.performative != ContractApiMessage.Performative.STATE - ): # pragma: nocover - self.context.logger.error( - f"An unexpected error occurred while checking {tx['hash'].hex()}: {contract_api_msg}" - ) - return None - - return cast(str, contract_api_msg.state.body["revert_reason"]) - - -class CheckLateTxHashesBehaviour( # pylint: disable=too-many-ancestors - CheckTransactionHistoryBehaviour -): - """Check the late-arriving transaction hashes.""" - - matching_round = CheckLateTxHashesRound - check_expected_to_be_verified = "One of the next tx checks" - - @property - def history(self) -> List[str]: - """Get the history of hashes.""" - return [ - hash_ - for hashes in self.synchronized_data.late_arriving_tx_hashes.values() - for hash_ in hashes - ] - - -class SynchronizeLateMessagesBehaviour(TransactionSettlementBaseBehaviour): - """Synchronize late-arriving messages behaviour.""" - - matching_round = SynchronizeLateMessagesRound - - def __init__(self, **kwargs: Any): - """Initialize a `SynchronizeLateMessagesBehaviour`""" - super().__init__(**kwargs) - # if we timed out during finalization, but we managed to receive a tx hash, - # then we sync it here by initializing the `_tx_hashes` with the unsynced hash. - self._tx_hashes: str = self.params.mutable_params.tx_hash - self._messages_iterator: Iterator[ContractApiMessage] = iter( - self.params.mutable_params.late_messages - ) - self.use_flashbots = False - - def setup(self) -> None: - """Setup the `SynchronizeLateMessagesBehaviour`.""" - tx_params = skill_input_hex_to_payload( - self.synchronized_data.most_voted_tx_hash - ) - self.use_flashbots = tx_params["use_flashbots"] - - def async_act(self) -> Generator: - """Do the action.""" - - with self.context.benchmark_tool.measure(self.behaviour_id).local(): - current_message = next(self._messages_iterator, None) - if current_message is not None: - chain_id = self.synchronized_data.get_chain_id( - self.params.default_chain_id - ) - tx_data = yield from self._get_tx_data( - current_message, - self.use_flashbots, - chain_id=chain_id, - ) - self.context.logger.info( - f"Found a late arriving message {current_message}. Result data: {tx_data}" - ) - # here, we concatenate the tx_hashes of all the late-arriving messages. Later, we will parse them. - self._tx_hashes += cast(str, tx_data["tx_digest"]) - return - - payload = SynchronizeLateMessagesPayload( - self.context.agent_address, self._tx_hashes - ) - - with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): - yield from self.send_a2a_transaction(payload) - # reset the local parameters if we were able to send them. - self.params.mutable_params.tx_hash = "" - self.params.mutable_params.late_messages = [] - yield from self.wait_until_round_end() - - self.set_done() - - -class SignatureBehaviour(TransactionSettlementBaseBehaviour): - """Signature behaviour.""" - - matching_round = CollectSignatureRound - - def async_act(self) -> Generator: - """ - Do the action. - - Steps: - - Request the signature of the transaction hash. - - Send the signature as a transaction and wait for it to be mined. - - Wait until ABCI application transitions to the next round. - - Go to the next behaviour (set done event). - """ - - with self.context.benchmark_tool.measure(self.behaviour_id).local(): - self.context.logger.info( - f"Agreement reached on tx data: {self.synchronized_data.most_voted_tx_hash}" - ) - signature_hex = yield from self._get_safe_tx_signature() - payload = SignaturePayload(self.context.agent_address, signature_hex) - - with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): - yield from self.send_a2a_transaction(payload) - yield from self.wait_until_round_end() - - self.set_done() - - def _get_safe_tx_signature(self) -> Generator[None, None, str]: - """Get signature of safe transaction hash.""" - tx_params = skill_input_hex_to_payload( - self.synchronized_data.most_voted_tx_hash - ) - # is_deprecated_mode=True because we want to call Account.signHash, - # which is the same used by gnosis-py - safe_tx_hash_bytes = binascii.unhexlify(tx_params["safe_tx_hash"]) - signature_hex = yield from self.get_signature( - safe_tx_hash_bytes, is_deprecated_mode=True - ) - # remove the leading '0x' - signature_hex = signature_hex[2:] - self.context.logger.info(f"Signature: {signature_hex}") - return signature_hex - - -class FinalizeBehaviour(TransactionSettlementBaseBehaviour): - """Finalize behaviour.""" - - matching_round = FinalizationRound - - def _i_am_not_sending(self) -> bool: - """Indicates if the current agent is the sender or not.""" - return ( - self.context.agent_address - != self.synchronized_data.most_voted_keeper_address - ) - - def async_act(self) -> Generator[None, None, None]: - """ - Do the action. - - Steps: - - If the agent is the keeper, then prepare the transaction and send it. - - Otherwise, wait until the next round. - - If a timeout is hit, set exit A event, otherwise set done event. - """ - if self._i_am_not_sending(): - yield from self._not_sender_act() - else: - yield from self._sender_act() - - def _not_sender_act(self) -> Generator: - """Do the non-sender action.""" - with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): - self.context.logger.info( - f"Waiting for the keeper to do its keeping: {self.synchronized_data.most_voted_keeper_address}" - ) - yield from self.wait_until_round_end() - self.set_done() - - def _sender_act(self) -> Generator[None, None, None]: - """Do the sender action.""" - - with self.context.benchmark_tool.measure(self.behaviour_id).local(): - self.context.logger.info( - "I am the designated sender, attempting to send the safe transaction..." - ) - tx_data = yield from self._send_safe_transaction() - if ( - tx_data["tx_digest"] == "" - and tx_data["status"] == VerificationStatus.PENDING - ) or tx_data["status"] == VerificationStatus.ERROR: - self.context.logger.error( - "Did not succeed with finalising the transaction!" - ) - elif tx_data["status"] == VerificationStatus.VERIFIED: - self.context.logger.error( - "Trying to finalize a transaction which has been verified already!" - ) - else: # pragma: no cover - self.context.logger.info( - f"Finalization tx digest: {cast(str, tx_data['tx_digest'])}" - ) - self.context.logger.debug( - f"Signatures: {pprint.pformat(self.synchronized_data.participant_to_signature)}" - ) - - tx_hashes_history = self.synchronized_data.tx_hashes_history - - if tx_data["tx_digest"] != "": - tx_hashes_history.append(cast(str, tx_data["tx_digest"])) - - tx_data_serialized = { - "status_value": cast(VerificationStatus, tx_data["status"]).value, - "serialized_keepers": self.serialized_keepers( - cast(Deque[str], tx_data["keepers"]), - cast(int, tx_data["keeper_retries"]), - ), - "blacklisted_keepers": "".join( - cast(Set[str], tx_data["blacklisted_keepers"]) - ), - "tx_hashes_history": "".join(tx_hashes_history), - "received_hash": bool(tx_data["tx_digest"]), - } - - payload = FinalizationTxPayload( - self.context.agent_address, - cast(Dict[str, Union[str, int, bool]], tx_data_serialized), - ) - - with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): - yield from self.send_a2a_transaction(payload) - # reset the local tx hash parameter if we were able to send it - self.params.mutable_params.tx_hash = "" - yield from self.wait_until_round_end() - - self.set_done() - - def _send_safe_transaction( - self, - ) -> Generator[None, None, TxDataType]: - """Send a Safe transaction using the participants' signatures.""" - tx_params = skill_input_hex_to_payload( - self.synchronized_data.most_voted_tx_hash - ) - chain_id = self.synchronized_data.get_chain_id(self.params.default_chain_id) - contract_api_msg = yield from self.get_contract_api_response( - performative=ContractApiMessage.Performative.GET_RAW_TRANSACTION, # type: ignore - contract_address=self.synchronized_data.safe_contract_address, - contract_id=str(GnosisSafeContract.contract_id), - contract_callable="get_raw_safe_transaction", - sender_address=self.context.agent_address, - owners=tuple(self.synchronized_data.participants), - to_address=tx_params["to_address"], - value=tx_params["ether_value"], - data=tx_params["data"], - safe_tx_gas=tx_params["safe_tx_gas"], - signatures_by_owner={ - key: payload.signature - for key, payload in self.synchronized_data.participant_to_signature.items() - }, - nonce=self.params.mutable_params.nonce, - old_price=self.params.mutable_params.gas_price, - operation=tx_params["operation"], - fallback_gas=self.params.mutable_params.fallback_gas, - gas_price=self.params.gas_params.gas_price, - max_fee_per_gas=self.params.gas_params.max_fee_per_gas, - max_priority_fee_per_gas=self.params.gas_params.max_priority_fee_per_gas, - chain_id=chain_id, - ) - - tx_data = yield from self._get_tx_data( - contract_api_msg, - tx_params["use_flashbots"], - tx_params["gas_limit"], - tx_params["raise_on_failed_simulation"], - chain_id, - ) - return tx_data - - def handle_late_messages(self, behaviour_id: str, message: Message) -> None: - """Store a potentially late-arriving message locally. - - :param behaviour_id: the id of the behaviour in which the message belongs to. - :param message: the late arriving message to handle. - """ - if ( - isinstance(message, ContractApiMessage) - and behaviour_id == self.behaviour_id - ): - self.context.logger.info(f"Late message arrived: {message}") - self.params.mutable_params.late_messages.append(message) - else: - super().handle_late_messages(behaviour_id, message) - - -class ResetBehaviour(TransactionSettlementBaseBehaviour): - """Reset behaviour.""" - - matching_round = ResetRound - - def async_act(self) -> Generator: - """Do the action.""" - self.context.logger.info( - f"Period {self.synchronized_data.period_count} was not finished. Resetting!" - ) - payload = ResetPayload( - self.context.agent_address, self.synchronized_data.period_count - ) - yield from self.send_a2a_transaction(payload) - yield from self.wait_until_round_end() - self.set_done() - - -class TransactionSettlementRoundBehaviour(AbstractRoundBehaviour): - """This behaviour manages the consensus stages for the basic transaction settlement.""" - - initial_behaviour_cls = RandomnessTransactionSubmissionBehaviour - abci_app_cls = TransactionSubmissionAbciApp - behaviours: Set[Type[BaseBehaviour]] = { - RandomnessTransactionSubmissionBehaviour, # type: ignore - SelectKeeperTransactionSubmissionBehaviourA, # type: ignore - SelectKeeperTransactionSubmissionBehaviourB, # type: ignore - SelectKeeperTransactionSubmissionBehaviourBAfterTimeout, # type: ignore - ValidateTransactionBehaviour, # type: ignore - CheckTransactionHistoryBehaviour, # type: ignore - SignatureBehaviour, # type: ignore - FinalizeBehaviour, # type: ignore - SynchronizeLateMessagesBehaviour, # type: ignore - CheckLateTxHashesBehaviour, # type: ignore - ResetBehaviour, # type: ignore - } diff --git a/packages/valory/skills/transaction_settlement_abci/dialogues.py b/packages/valory/skills/transaction_settlement_abci/dialogues.py deleted file mode 100644 index e244bde..0000000 --- a/packages/valory/skills/transaction_settlement_abci/dialogues.py +++ /dev/null @@ -1,91 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the classes required for dialogue management.""" - -from packages.valory.skills.abstract_round_abci.dialogues import ( - AbciDialogue as BaseAbciDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - AbciDialogues as BaseAbciDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - ContractApiDialogue as BaseContractApiDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - ContractApiDialogues as BaseContractApiDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - HttpDialogue as BaseHttpDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - HttpDialogues as BaseHttpDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - IpfsDialogue as BaseIpfsDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - IpfsDialogues as BaseIpfsDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - LedgerApiDialogue as BaseLedgerApiDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - LedgerApiDialogues as BaseLedgerApiDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - SigningDialogue as BaseSigningDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - SigningDialogues as BaseSigningDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - TendermintDialogue as BaseTendermintDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - TendermintDialogues as BaseTendermintDialogues, -) - - -AbciDialogue = BaseAbciDialogue -AbciDialogues = BaseAbciDialogues - - -HttpDialogue = BaseHttpDialogue -HttpDialogues = BaseHttpDialogues - - -SigningDialogue = BaseSigningDialogue -SigningDialogues = BaseSigningDialogues - - -LedgerApiDialogue = BaseLedgerApiDialogue -LedgerApiDialogues = BaseLedgerApiDialogues - - -ContractApiDialogue = BaseContractApiDialogue -ContractApiDialogues = BaseContractApiDialogues - - -TendermintDialogue = BaseTendermintDialogue -TendermintDialogues = BaseTendermintDialogues - - -IpfsDialogue = BaseIpfsDialogue -IpfsDialogues = BaseIpfsDialogues diff --git a/packages/valory/skills/transaction_settlement_abci/fsm_specification.yaml b/packages/valory/skills/transaction_settlement_abci/fsm_specification.yaml deleted file mode 100644 index 70b6e1f..0000000 --- a/packages/valory/skills/transaction_settlement_abci/fsm_specification.yaml +++ /dev/null @@ -1,88 +0,0 @@ -alphabet_in: -- CHECK_HISTORY -- CHECK_LATE_ARRIVING_MESSAGE -- CHECK_TIMEOUT -- DONE -- FINALIZATION_FAILED -- FINALIZE_TIMEOUT -- INCORRECT_SERIALIZATION -- INSUFFICIENT_FUNDS -- NEGATIVE -- NONE -- NO_MAJORITY -- RESET_TIMEOUT -- ROUND_TIMEOUT -- SUSPICIOUS_ACTIVITY -- VALIDATE_TIMEOUT -default_start_state: RandomnessTransactionSubmissionRound -final_states: -- FailedRound -- FinishedTransactionSubmissionRound -label: TransactionSubmissionAbciApp -start_states: -- RandomnessTransactionSubmissionRound -states: -- CheckLateTxHashesRound -- CheckTransactionHistoryRound -- CollectSignatureRound -- FailedRound -- FinalizationRound -- FinishedTransactionSubmissionRound -- RandomnessTransactionSubmissionRound -- ResetRound -- SelectKeeperTransactionSubmissionARound -- SelectKeeperTransactionSubmissionBAfterTimeoutRound -- SelectKeeperTransactionSubmissionBRound -- SynchronizeLateMessagesRound -- ValidateTransactionRound -transition_func: - (CheckLateTxHashesRound, CHECK_LATE_ARRIVING_MESSAGE): SynchronizeLateMessagesRound - (CheckLateTxHashesRound, CHECK_TIMEOUT): CheckLateTxHashesRound - (CheckLateTxHashesRound, DONE): FinishedTransactionSubmissionRound - (CheckLateTxHashesRound, NEGATIVE): FailedRound - (CheckLateTxHashesRound, NONE): FailedRound - (CheckLateTxHashesRound, NO_MAJORITY): FailedRound - (CheckTransactionHistoryRound, CHECK_LATE_ARRIVING_MESSAGE): SynchronizeLateMessagesRound - (CheckTransactionHistoryRound, CHECK_TIMEOUT): CheckTransactionHistoryRound - (CheckTransactionHistoryRound, DONE): FinishedTransactionSubmissionRound - (CheckTransactionHistoryRound, NEGATIVE): SelectKeeperTransactionSubmissionBRound - (CheckTransactionHistoryRound, NONE): FailedRound - (CheckTransactionHistoryRound, NO_MAJORITY): CheckTransactionHistoryRound - (CollectSignatureRound, DONE): FinalizationRound - (CollectSignatureRound, NO_MAJORITY): ResetRound - (CollectSignatureRound, ROUND_TIMEOUT): CollectSignatureRound - (FinalizationRound, CHECK_HISTORY): CheckTransactionHistoryRound - (FinalizationRound, CHECK_LATE_ARRIVING_MESSAGE): SynchronizeLateMessagesRound - (FinalizationRound, DONE): ValidateTransactionRound - (FinalizationRound, FINALIZATION_FAILED): SelectKeeperTransactionSubmissionBRound - (FinalizationRound, FINALIZE_TIMEOUT): SelectKeeperTransactionSubmissionBAfterTimeoutRound - (FinalizationRound, INSUFFICIENT_FUNDS): SelectKeeperTransactionSubmissionBRound - (RandomnessTransactionSubmissionRound, DONE): SelectKeeperTransactionSubmissionARound - (RandomnessTransactionSubmissionRound, NO_MAJORITY): RandomnessTransactionSubmissionRound - (RandomnessTransactionSubmissionRound, ROUND_TIMEOUT): RandomnessTransactionSubmissionRound - (ResetRound, DONE): RandomnessTransactionSubmissionRound - (ResetRound, NO_MAJORITY): FailedRound - (ResetRound, RESET_TIMEOUT): FailedRound - (SelectKeeperTransactionSubmissionARound, DONE): CollectSignatureRound - (SelectKeeperTransactionSubmissionARound, INCORRECT_SERIALIZATION): FailedRound - (SelectKeeperTransactionSubmissionARound, NO_MAJORITY): ResetRound - (SelectKeeperTransactionSubmissionARound, ROUND_TIMEOUT): SelectKeeperTransactionSubmissionARound - (SelectKeeperTransactionSubmissionBAfterTimeoutRound, CHECK_HISTORY): CheckTransactionHistoryRound - (SelectKeeperTransactionSubmissionBAfterTimeoutRound, CHECK_LATE_ARRIVING_MESSAGE): SynchronizeLateMessagesRound - (SelectKeeperTransactionSubmissionBAfterTimeoutRound, DONE): FinalizationRound - (SelectKeeperTransactionSubmissionBAfterTimeoutRound, INCORRECT_SERIALIZATION): FailedRound - (SelectKeeperTransactionSubmissionBAfterTimeoutRound, NO_MAJORITY): ResetRound - (SelectKeeperTransactionSubmissionBAfterTimeoutRound, ROUND_TIMEOUT): SelectKeeperTransactionSubmissionBAfterTimeoutRound - (SelectKeeperTransactionSubmissionBRound, DONE): FinalizationRound - (SelectKeeperTransactionSubmissionBRound, INCORRECT_SERIALIZATION): FailedRound - (SelectKeeperTransactionSubmissionBRound, NO_MAJORITY): ResetRound - (SelectKeeperTransactionSubmissionBRound, ROUND_TIMEOUT): SelectKeeperTransactionSubmissionBRound - (SynchronizeLateMessagesRound, DONE): CheckLateTxHashesRound - (SynchronizeLateMessagesRound, NONE): SelectKeeperTransactionSubmissionBRound - (SynchronizeLateMessagesRound, ROUND_TIMEOUT): SynchronizeLateMessagesRound - (SynchronizeLateMessagesRound, SUSPICIOUS_ACTIVITY): FailedRound - (ValidateTransactionRound, DONE): FinishedTransactionSubmissionRound - (ValidateTransactionRound, NEGATIVE): CheckTransactionHistoryRound - (ValidateTransactionRound, NONE): SelectKeeperTransactionSubmissionBRound - (ValidateTransactionRound, NO_MAJORITY): ValidateTransactionRound - (ValidateTransactionRound, VALIDATE_TIMEOUT): CheckTransactionHistoryRound diff --git a/packages/valory/skills/transaction_settlement_abci/handlers.py b/packages/valory/skills/transaction_settlement_abci/handlers.py deleted file mode 100644 index 9a4990b..0000000 --- a/packages/valory/skills/transaction_settlement_abci/handlers.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the handler for the 'transaction_settlement_abci' skill.""" - -from packages.valory.skills.abstract_round_abci.handlers import ( - ABCIRoundHandler as BaseABCIRoundHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - ContractApiHandler as BaseContractApiHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - HttpHandler as BaseHttpHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - IpfsHandler as BaseIpfsHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - LedgerApiHandler as BaseLedgerApiHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - SigningHandler as BaseSigningHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - TendermintHandler as BaseTendermintHandler, -) - - -ABCIHandler = BaseABCIRoundHandler -HttpHandler = BaseHttpHandler -SigningHandler = BaseSigningHandler -LedgerApiHandler = BaseLedgerApiHandler -ContractApiHandler = BaseContractApiHandler -TendermintHandler = BaseTendermintHandler -IpfsHandler = BaseIpfsHandler diff --git a/packages/valory/skills/transaction_settlement_abci/models.py b/packages/valory/skills/transaction_settlement_abci/models.py deleted file mode 100644 index e65bb69..0000000 --- a/packages/valory/skills/transaction_settlement_abci/models.py +++ /dev/null @@ -1,123 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Custom objects for the transaction settlement ABCI application.""" -from dataclasses import dataclass, field -from typing import Any, Dict, List, Optional - -from web3.types import Nonce, Wei - -from packages.valory.protocols.contract_api import ContractApiMessage -from packages.valory.skills.abstract_round_abci.models import ApiSpecs, BaseParams -from packages.valory.skills.abstract_round_abci.models import ( - BenchmarkTool as BaseBenchmarkTool, -) -from packages.valory.skills.abstract_round_abci.models import Requests as BaseRequests -from packages.valory.skills.abstract_round_abci.models import ( - SharedState as BaseSharedState, -) -from packages.valory.skills.abstract_round_abci.models import TypeCheckMixin -from packages.valory.skills.transaction_settlement_abci.rounds import ( - TransactionSubmissionAbciApp, -) - - -_MINIMUM_VALIDATE_TIMEOUT = 300 # 5 minutes -BenchmarkTool = BaseBenchmarkTool - - -class SharedState(BaseSharedState): - """Keep the current shared state of the skill.""" - - abci_app_cls = TransactionSubmissionAbciApp - - -@dataclass -class MutableParams(TypeCheckMixin): - """Collection for the mutable parameters.""" - - fallback_gas: int - tx_hash: str = "" - nonce: Optional[Nonce] = None - gas_price: Optional[Dict[str, Wei]] = None - late_messages: List[ContractApiMessage] = field(default_factory=list) - - -@dataclass -class GasParams(BaseParams): - """Gas parameters.""" - - gas_price: Optional[int] = None - max_fee_per_gas: Optional[int] = None - max_priority_fee_per_gas: Optional[int] = None - - -class TransactionParams(BaseParams): # pylint: disable=too-many-instance-attributes - """Transaction settlement agent-specific parameters.""" - - def __init__(self, *args: Any, **kwargs: Any) -> None: - """ - Initialize the parameters object. - - We keep track of the nonce and tip across rounds and periods. - We reuse it each time a new raw transaction is generated. If - at the time of the new raw transaction being generated the nonce - on the ledger does not match the nonce on the skill, then we ignore - the skill nonce and tip (effectively we price fresh). Otherwise, we - are in a re-submission scenario where we need to take account of the - old tip. - - :param args: positional arguments - :param kwargs: keyword arguments - """ - self.mutable_params = MutableParams( - fallback_gas=self._ensure("init_fallback_gas", kwargs, int) - ) - self.keeper_allowed_retries: int = self._ensure( - "keeper_allowed_retries", kwargs, int - ) - self.validate_timeout: int = self._ensure_gte( - "validate_timeout", - kwargs, - int, - min_value=_MINIMUM_VALIDATE_TIMEOUT, - ) - self.finalize_timeout: float = self._ensure("finalize_timeout", kwargs, float) - self.history_check_timeout: int = self._ensure( - "history_check_timeout", kwargs, int - ) - self.gas_params = self._get_gas_params(kwargs) - super().__init__(*args, **kwargs) - - @staticmethod - def _get_gas_params(kwargs: Dict[str, Any]) -> GasParams: - """Get the gas parameters.""" - gas_params = kwargs.pop("gas_params", {}) - gas_price = gas_params.get("gas_price", None) - max_fee_per_gas = gas_params.get("max_fee_per_gas", None) - max_priority_fee_per_gas = gas_params.get("max_priority_fee_per_gas", None) - return GasParams( - gas_price=gas_price, - max_fee_per_gas=max_fee_per_gas, - max_priority_fee_per_gas=max_priority_fee_per_gas, - ) - - -RandomnessApi = ApiSpecs -Requests = BaseRequests diff --git a/packages/valory/skills/transaction_settlement_abci/payload_tools.py b/packages/valory/skills/transaction_settlement_abci/payload_tools.py deleted file mode 100644 index 44a8688..0000000 --- a/packages/valory/skills/transaction_settlement_abci/payload_tools.py +++ /dev/null @@ -1,183 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2022-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Tools for payload serialization and deserialization.""" - -from enum import Enum -from typing import Any, Optional, Tuple - -from packages.valory.contracts.gnosis_safe.contract import SafeOperation - - -NULL_ADDRESS: str = "0x" + "0" * 40 -MAX_UINT256 = 2**256 - 1 - - -class VerificationStatus(Enum): - """Tx verification status enumeration.""" - - VERIFIED = 1 - NOT_VERIFIED = 2 - INVALID_PAYLOAD = 3 - PENDING = 4 - ERROR = 5 - INSUFFICIENT_FUNDS = 6 - BAD_SAFE_NONCE = 7 - - -class PayloadDeserializationError(Exception): - """Exception for payload deserialization errors.""" - - def __init__(self, *args: Any) -> None: - """Initialize the exception. - - :param args: extra arguments to pass to the constructor of `Exception`. - """ - msg: str = "Cannot decode provided payload!" - if not args: - args = (msg,) - - super().__init__(*args) - - -def tx_hist_payload_to_hex( - verification: VerificationStatus, tx_hash: Optional[str] = None -) -> str: - """Serialise history payload to a hex string.""" - if tx_hash is None: - tx_hash = "" - else: - tx_hash = tx_hash[2:] if tx_hash.startswith("0x") else tx_hash - if len(tx_hash) != 64: - raise ValueError("Cannot encode tx_hash of non-32 bytes") - verification_ = verification.value.to_bytes(32, "big").hex() - concatenated = verification_ + tx_hash - return concatenated - - -def tx_hist_hex_to_payload(payload: str) -> Tuple[VerificationStatus, Optional[str]]: - """Decode history payload.""" - if len(payload) != 64 and len(payload) != 64 * 2: - raise PayloadDeserializationError() - - verification_value = int.from_bytes(bytes.fromhex(payload[:64]), "big") - - try: - verification_status = VerificationStatus(verification_value) - except ValueError as e: - raise PayloadDeserializationError(str(e)) from e - - if len(payload) == 64: - return verification_status, None - - return verification_status, "0x" + payload[64:] - - -def hash_payload_to_hex( # pylint: disable=too-many-arguments, too-many-locals - safe_tx_hash: str, - ether_value: int, - safe_tx_gas: int, - to_address: str, - data: bytes, - operation: int = SafeOperation.CALL.value, - base_gas: int = 0, - safe_gas_price: int = 0, - gas_token: str = NULL_ADDRESS, - refund_receiver: str = NULL_ADDRESS, - use_flashbots: bool = False, - gas_limit: int = 0, - raise_on_failed_simulation: bool = False, -) -> str: - """Serialise to a hex string.""" - if len(safe_tx_hash) != 64: # should be exactly 32 bytes! - raise ValueError( - "cannot encode safe_tx_hash of non-32 bytes" - ) # pragma: nocover - - if len(to_address) != 42 or len(gas_token) != 42 or len(refund_receiver) != 42: - raise ValueError("cannot encode address of non 42 length") # pragma: nocover - - if ( - ether_value > MAX_UINT256 - or safe_tx_gas > MAX_UINT256 - or base_gas > MAX_UINT256 - or safe_gas_price > MAX_UINT256 - or gas_limit > MAX_UINT256 - ): - raise ValueError( - "Value is bigger than the max 256 bit value" - ) # pragma: nocover - - if operation not in [v.value for v in SafeOperation]: - raise ValueError("SafeOperation value is not valid") # pragma: nocover - - if not isinstance(use_flashbots, bool): - raise ValueError( - f"`use_flashbots` value ({use_flashbots}) is not valid. A boolean value was expected instead" - ) - - ether_value_ = ether_value.to_bytes(32, "big").hex() - safe_tx_gas_ = safe_tx_gas.to_bytes(32, "big").hex() - operation_ = operation.to_bytes(1, "big").hex() - base_gas_ = base_gas.to_bytes(32, "big").hex() - safe_gas_price_ = safe_gas_price.to_bytes(32, "big").hex() - use_flashbots_ = use_flashbots.to_bytes(32, "big").hex() - gas_limit_ = gas_limit.to_bytes(32, "big").hex() - raise_on_failed_simulation_ = raise_on_failed_simulation.to_bytes(32, "big").hex() - - concatenated = ( - safe_tx_hash - + ether_value_ - + safe_tx_gas_ - + to_address - + operation_ - + base_gas_ - + safe_gas_price_ - + gas_token - + refund_receiver - + use_flashbots_ - + gas_limit_ - + raise_on_failed_simulation_ - + data.hex() - ) - return concatenated - - -def skill_input_hex_to_payload(payload: str) -> dict: - """Decode payload.""" - if len(payload) < 234: - raise PayloadDeserializationError() # pragma: nocover - tx_params = dict( - safe_tx_hash=payload[:64], - ether_value=int.from_bytes(bytes.fromhex(payload[64:128]), "big"), - safe_tx_gas=int.from_bytes(bytes.fromhex(payload[128:192]), "big"), - to_address=payload[192:234], - operation=int.from_bytes(bytes.fromhex(payload[234:236]), "big"), - base_gas=int.from_bytes(bytes.fromhex(payload[236:300]), "big"), - safe_gas_price=int.from_bytes(bytes.fromhex(payload[300:364]), "big"), - gas_token=payload[364:406], - refund_receiver=payload[406:448], - use_flashbots=bool.from_bytes(bytes.fromhex(payload[448:512]), "big"), - gas_limit=int.from_bytes(bytes.fromhex(payload[512:576]), "big"), - raise_on_failed_simulation=bool.from_bytes( - bytes.fromhex(payload[576:640]), "big" - ), - data=bytes.fromhex(payload[640:]), - ) - return tx_params diff --git a/packages/valory/skills/transaction_settlement_abci/payloads.py b/packages/valory/skills/transaction_settlement_abci/payloads.py deleted file mode 100644 index 16dad1e..0000000 --- a/packages/valory/skills/transaction_settlement_abci/payloads.py +++ /dev/null @@ -1,82 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the transaction payloads for common apps.""" - -from dataclasses import dataclass -from typing import Dict, Optional, Union - -from packages.valory.skills.abstract_round_abci.base import BaseTxPayload - - -@dataclass(frozen=True) -class RandomnessPayload(BaseTxPayload): - """Represent a transaction payload of type 'randomness'.""" - - round_id: int - randomness: str - - -@dataclass(frozen=True) -class SelectKeeperPayload(BaseTxPayload): - """Represent a transaction payload of type 'select_keeper'.""" - - keepers: str - - -@dataclass(frozen=True) -class ValidatePayload(BaseTxPayload): - """Represent a transaction payload of type 'validate'.""" - - vote: Optional[bool] = None - - -@dataclass(frozen=True) -class CheckTransactionHistoryPayload(BaseTxPayload): - """Represent a transaction payload of type 'check'.""" - - verified_res: str - - -@dataclass(frozen=True) -class SynchronizeLateMessagesPayload(BaseTxPayload): - """Represent a transaction payload of type 'synchronize'.""" - - tx_hashes: str - - -@dataclass(frozen=True) -class SignaturePayload(BaseTxPayload): - """Represent a transaction payload of type 'signature'.""" - - signature: str - - -@dataclass(frozen=True) -class FinalizationTxPayload(BaseTxPayload): - """Represent a transaction payload of type 'finalization'.""" - - tx_data: Optional[Dict[str, Union[str, int, bool]]] = None - - -@dataclass(frozen=True) -class ResetPayload(BaseTxPayload): - """Represent a transaction payload of type 'reset'.""" - - period_count: int diff --git a/packages/valory/skills/transaction_settlement_abci/rounds.py b/packages/valory/skills/transaction_settlement_abci/rounds.py deleted file mode 100644 index d6231ee..0000000 --- a/packages/valory/skills/transaction_settlement_abci/rounds.py +++ /dev/null @@ -1,831 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains the data classes for the `transaction settlement` ABCI application.""" - -import textwrap -from abc import ABC -from collections import deque -from enum import Enum -from typing import Deque, Dict, List, Mapping, Optional, Set, Tuple, cast - -from packages.valory.skills.abstract_round_abci.base import ( - ABCIAppInternalError, - AbciApp, - AbciAppTransitionFunction, - AppState, - BaseSynchronizedData, - BaseTxPayload, - CollectDifferentUntilThresholdRound, - CollectNonEmptyUntilThresholdRound, - CollectSameUntilThresholdRound, - CollectionRound, - DegenerateRound, - OnlyKeeperSendsRound, - TransactionNotValidError, - VALUE_NOT_PROVIDED, - VotingRound, - get_name, -) -from packages.valory.skills.abstract_round_abci.utils import filter_negative -from packages.valory.skills.transaction_settlement_abci.payload_tools import ( - VerificationStatus, - tx_hist_hex_to_payload, -) -from packages.valory.skills.transaction_settlement_abci.payloads import ( - CheckTransactionHistoryPayload, - FinalizationTxPayload, - RandomnessPayload, - ResetPayload, - SelectKeeperPayload, - SignaturePayload, - SynchronizeLateMessagesPayload, - ValidatePayload, -) - - -ADDRESS_LENGTH = 42 -TX_HASH_LENGTH = 66 -RETRIES_LENGTH = 64 - - -class Event(Enum): - """Event enumeration for the price estimation demo.""" - - DONE = "done" - ROUND_TIMEOUT = "round_timeout" - NO_MAJORITY = "no_majority" - NEGATIVE = "negative" - NONE = "none" - FINALIZE_TIMEOUT = "finalize_timeout" - VALIDATE_TIMEOUT = "validate_timeout" - CHECK_TIMEOUT = "check_timeout" - RESET_TIMEOUT = "reset_timeout" - CHECK_HISTORY = "check_history" - CHECK_LATE_ARRIVING_MESSAGE = "check_late_arriving_message" - FINALIZATION_FAILED = "finalization_failed" - SUSPICIOUS_ACTIVITY = "suspicious_activity" - INSUFFICIENT_FUNDS = "insufficient_funds" - INCORRECT_SERIALIZATION = "incorrect_serialization" - - -class SynchronizedData( - BaseSynchronizedData -): # pylint: disable=too-many-instance-attributes - """ - Class to represent the synchronized data. - - This data is replicated by the tendermint application. - """ - - @property - def participant_to_signature(self) -> Mapping[str, SignaturePayload]: - """Get the participant_to_signature.""" - serialized = self.db.get_strict("participant_to_signature") - deserialized = CollectionRound.deserialize_collection(serialized) - return cast(Mapping[str, SignaturePayload], deserialized) - - @property - def tx_hashes_history(self) -> List[str]: - """Get the current cycle's tx hashes history, which has not yet been verified.""" - raw = cast(str, self.db.get("tx_hashes_history", "")) - return textwrap.wrap(raw, TX_HASH_LENGTH) - - @property - def keepers(self) -> Deque[str]: - """Get the current cycle's keepers who have tried to submit a transaction.""" - if self.is_keeper_set: - keepers_unparsed = cast(str, self.db.get_strict("keepers")) - keepers_parsed = textwrap.wrap( - keepers_unparsed[RETRIES_LENGTH:], ADDRESS_LENGTH - ) - return deque(keepers_parsed) - return deque() - - @property - def keepers_threshold_exceeded(self) -> bool: - """Check if the number of selected keepers has exceeded the allowed limit.""" - malicious_threshold = self.nb_participants // 3 - return len(self.keepers) > malicious_threshold - - @property - def most_voted_randomness_round(self) -> int: # pragma: no cover - """Get the first in priority keeper to try to re-submit a transaction.""" - round_ = self.db.get_strict("most_voted_randomness_round") - return cast(int, round_) - - @property - def most_voted_keeper_address(self) -> str: - """Get the first in priority keeper to try to re-submit a transaction.""" - return self.keepers[0] - - @property # TODO: overrides base property, investigate - def is_keeper_set(self) -> bool: - """Check whether keeper is set.""" - return bool(self.db.get("keepers", False)) - - @property - def keeper_retries(self) -> int: - """Get the number of times the current keeper has retried.""" - if self.is_keeper_set: - keepers_unparsed = cast(str, self.db.get_strict("keepers")) - keeper_retries = int.from_bytes( - bytes.fromhex(keepers_unparsed[:RETRIES_LENGTH]), "big" - ) - return keeper_retries - return 0 - - @property - def to_be_validated_tx_hash(self) -> str: - """ - Get the tx hash which is ready for validation. - - This will always be the last hash in the `tx_hashes_history`, - due to the way we are inserting the hashes in the array. - We keep the hashes sorted by the time of their finalization. - If this property is accessed before the finalization succeeds, - then it is incorrectly used and raises an error. - - :return: the tx hash which is ready for validation. - """ - if not self.tx_hashes_history: - raise ValueError( - "FSM design error: tx hash should exist" - ) # pragma: no cover - return self.tx_hashes_history[-1] - - @property - def final_tx_hash(self) -> str: - """Get the verified tx hash.""" - return cast(str, self.db.get_strict("final_tx_hash")) - - @property - def final_verification_status(self) -> VerificationStatus: - """Get the final verification status.""" - status_value = self.db.get("final_verification_status", None) - if status_value is None: - return VerificationStatus.NOT_VERIFIED - return VerificationStatus(status_value) - - @property - def most_voted_tx_hash(self) -> str: - """Get the most_voted_tx_hash.""" - return cast(str, self.db.get_strict("most_voted_tx_hash")) - - @property - def missed_messages(self) -> Dict[str, int]: - """The number of missed messages per agent address.""" - default = dict.fromkeys(self.all_participants, 0) - missed_messages = self.db.get("missed_messages", default) - return cast(Dict[str, int], missed_messages) - - @property - def n_missed_messages(self) -> int: - """The number of missed messages in total.""" - return sum(self.missed_messages.values()) - - @property - def should_check_late_messages(self) -> bool: - """Check if we should check for late-arriving messages.""" - return self.n_missed_messages > 0 - - @property - def late_arriving_tx_hashes(self) -> Dict[str, List[str]]: - """Get the late_arriving_tx_hashes.""" - late_arrivals = cast( - Dict[str, str], self.db.get_strict("late_arriving_tx_hashes") - ) - parsed_hashes = { - sender: textwrap.wrap(hashes, TX_HASH_LENGTH) - for sender, hashes in late_arrivals.items() - } - return parsed_hashes - - @property - def suspects(self) -> Tuple[str]: - """Get the suspect agents.""" - return cast(Tuple[str], self.db.get("suspects", tuple())) - - @property - def most_voted_check_result(self) -> str: # pragma: no cover - """Get the most voted checked result.""" - return cast(str, self.db.get_strict("most_voted_check_result")) - - @property - def participant_to_check( - self, - ) -> Mapping[str, CheckTransactionHistoryPayload]: # pragma: no cover - """Get the mapping from participants to checks.""" - serialized = self.db.get_strict("participant_to_check") - deserialized = CollectionRound.deserialize_collection(serialized) - return cast(Mapping[str, CheckTransactionHistoryPayload], deserialized) - - @property - def participant_to_late_messages( - self, - ) -> Mapping[str, SynchronizeLateMessagesPayload]: # pragma: no cover - """Get the mapping from participants to checks.""" - serialized = self.db.get_strict("participant_to_late_message") - deserialized = CollectionRound.deserialize_collection(serialized) - return cast(Mapping[str, SynchronizeLateMessagesPayload], deserialized) - - def get_chain_id(self, default_chain_id: str) -> str: - """Get the chain id.""" - return cast(str, self.db.get("chain_id", default_chain_id)) - - -class FailedRound(DegenerateRound, ABC): - """A round that represents that the period failed""" - - -class CollectSignatureRound(CollectDifferentUntilThresholdRound): - """A round in which agents sign the transaction""" - - payload_class = SignaturePayload - synchronized_data_class = SynchronizedData - done_event = Event.DONE - no_majority_event = Event.NO_MAJORITY - collection_key = get_name(SynchronizedData.participant_to_signature) - - -class FinalizationRound(OnlyKeeperSendsRound): - """A round that represents transaction signing has finished""" - - keeper_payload: Optional[FinalizationTxPayload] = None - payload_class = FinalizationTxPayload - synchronized_data_class = SynchronizedData - - def end_block( # pylint: disable=too-many-return-statements - self, - ) -> Optional[ - Tuple[BaseSynchronizedData, Enum] - ]: # pylint: disable=too-many-return-statements - """Process the end of the block.""" - if self.keeper_payload is None: - return None - - if self.keeper_payload.tx_data is None: - return self.synchronized_data, Event.FINALIZATION_FAILED - - verification_status = VerificationStatus( - self.keeper_payload.tx_data["status_value"] - ) - synchronized_data = cast( - SynchronizedData, - self.synchronized_data.update( - synchronized_data_class=self.synchronized_data_class, - **{ - get_name( - SynchronizedData.tx_hashes_history - ): self.keeper_payload.tx_data["tx_hashes_history"], - get_name( - SynchronizedData.final_verification_status - ): verification_status.value, - get_name(SynchronizedData.keepers): self.keeper_payload.tx_data[ - "serialized_keepers" - ], - get_name( - SynchronizedData.blacklisted_keepers - ): self.keeper_payload.tx_data["blacklisted_keepers"], - }, - ), - ) - - # check if we succeeded in finalization. - # we may fail in any of the following cases: - # 1. Getting raw safe transaction. - # 2. Requesting transaction signature. - # 3. Requesting transaction digest. - if self.keeper_payload.tx_data["received_hash"]: - return synchronized_data, Event.DONE - # If keeper has been blacklisted, return an `INSUFFICIENT_FUNDS` event. - if verification_status == VerificationStatus.INSUFFICIENT_FUNDS: - return synchronized_data, Event.INSUFFICIENT_FUNDS - # This means that getting raw safe transaction succeeded, - # but either requesting tx signature or requesting tx digest failed. - if verification_status not in ( - VerificationStatus.ERROR, - VerificationStatus.VERIFIED, - ): - return synchronized_data, Event.FINALIZATION_FAILED - # if there is a tx hash history, then check it for validated txs. - if synchronized_data.tx_hashes_history: - return synchronized_data, Event.CHECK_HISTORY - # if there could be any late messages, check if any has arrived. - if synchronized_data.should_check_late_messages: - return synchronized_data, Event.CHECK_LATE_ARRIVING_MESSAGE - # otherwise fail. - return synchronized_data, Event.FINALIZATION_FAILED - - -class RandomnessTransactionSubmissionRound(CollectSameUntilThresholdRound): - """A round for generating randomness""" - - payload_class = RandomnessPayload - synchronized_data_class = SynchronizedData - done_event = Event.DONE - no_majority_event = Event.NO_MAJORITY - collection_key = get_name(SynchronizedData.participant_to_randomness) - selection_key = ( - get_name(SynchronizedData.most_voted_randomness_round), - get_name(SynchronizedData.most_voted_randomness), - ) - - -class SelectKeeperTransactionSubmissionARound(CollectSameUntilThresholdRound): - """A round in which a keeper is selected for transaction submission""" - - payload_class = SelectKeeperPayload - synchronized_data_class = SynchronizedData - done_event = Event.DONE - no_majority_event = Event.NO_MAJORITY - collection_key = get_name(SynchronizedData.participant_to_selection) - selection_key = get_name(SynchronizedData.keepers) - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: - """Process the end of the block.""" - - if self.threshold_reached and self.most_voted_payload is not None: - if ( - len(self.most_voted_payload) < RETRIES_LENGTH + ADDRESS_LENGTH - or (len(self.most_voted_payload) - RETRIES_LENGTH) % ADDRESS_LENGTH != 0 - ): - # if we cannot parse the keepers' payload, then the developer has serialized it incorrectly. - return self.synchronized_data, Event.INCORRECT_SERIALIZATION - - return super().end_block() - - -class SelectKeeperTransactionSubmissionBRound(SelectKeeperTransactionSubmissionARound): - """A round in which a new keeper is selected for transaction submission""" - - -class SelectKeeperTransactionSubmissionBAfterTimeoutRound( - SelectKeeperTransactionSubmissionBRound -): - """A round in which a new keeper is selected for tx submission after a round timeout of the previous keeper""" - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: - """Process the end of the block.""" - if self.threshold_reached: - synchronized_data = cast(SynchronizedData, self.synchronized_data) - keeper = synchronized_data.most_voted_keeper_address - missed_messages = synchronized_data.missed_messages - missed_messages[keeper] += 1 - - synchronized_data = cast( - SynchronizedData, - self.synchronized_data.update( - synchronized_data_class=self.synchronized_data_class, - **{get_name(SynchronizedData.missed_messages): missed_messages}, - ), - ) - if synchronized_data.keepers_threshold_exceeded: - # we only stop re-selection if there are any previous transaction hashes or any missed messages. - if len(synchronized_data.tx_hashes_history) > 0: - return synchronized_data, Event.CHECK_HISTORY - if synchronized_data.should_check_late_messages: - return synchronized_data, Event.CHECK_LATE_ARRIVING_MESSAGE - return super().end_block() - - -class ValidateTransactionRound(VotingRound): - """A round in which agents validate the transaction""" - - payload_class = ValidatePayload - synchronized_data_class = SynchronizedData - done_event = Event.DONE - negative_event = Event.NEGATIVE - none_event = Event.NONE - no_majority_event = Event.NO_MAJORITY - collection_key = get_name(SynchronizedData.participant_to_votes) - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: - """Process the end of the block.""" - # if reached participant threshold, set the result - - if self.positive_vote_threshold_reached: - # We obtain the latest tx hash from the `tx_hashes_history`. - # We keep the hashes sorted by their finalization time. - # If this property is accessed before the finalization succeeds, - # then it is incorrectly used. - final_tx_hash = cast( - SynchronizedData, self.synchronized_data - ).to_be_validated_tx_hash - - # We only set the final tx hash if we are about to exit from the transaction settlement skill. - # Then, the skills which use the transaction settlement can check the tx hash - # and if it is None, then it means that the transaction has failed. - synchronized_data = self.synchronized_data.update( - synchronized_data_class=self.synchronized_data_class, - **{ - self.collection_key: self.serialized_collection, - get_name( - SynchronizedData.final_verification_status - ): VerificationStatus.VERIFIED.value, - get_name(SynchronizedData.final_tx_hash): final_tx_hash, - }, - ) - return synchronized_data, self.done_event - if self.negative_vote_threshold_reached: - return self.synchronized_data, self.negative_event - if self.none_vote_threshold_reached: - return self.synchronized_data, self.none_event - if not self.is_majority_possible( - self.collection, self.synchronized_data.nb_participants - ): - return self.synchronized_data, self.no_majority_event - return None - - -class CheckTransactionHistoryRound(CollectSameUntilThresholdRound): - """A round in which agents check the transaction history to see if any previous tx has been validated""" - - payload_class = CheckTransactionHistoryPayload - synchronized_data_class = SynchronizedData - collection_key = get_name(SynchronizedData.participant_to_check) - selection_key = get_name(SynchronizedData.most_voted_check_result) - - def end_block( # pylint: disable=too-many-return-statements - self, - ) -> Optional[Tuple[BaseSynchronizedData, Enum]]: - """Process the end of the block.""" - if self.threshold_reached: - return_status, return_tx_hash = tx_hist_hex_to_payload( - self.most_voted_payload - ) - - if return_status == VerificationStatus.NOT_VERIFIED: - # We don't update the synchronized_data as we need to repeat all checks again later - synchronized_data = self.synchronized_data - else: - # We only set the final tx hash if we are about to exit from the transaction settlement skill. - # Then, the skills which use the transaction settlement can check the tx hash - # and if it is None, then it means that the transaction has failed. - synchronized_data = self.synchronized_data.update( - synchronized_data_class=self.synchronized_data_class, - **{ - self.collection_key: self.serialized_collection, - self.selection_key: self.most_voted_payload, - get_name( - SynchronizedData.final_verification_status - ): return_status.value, - get_name(SynchronizedData.final_tx_hash): return_tx_hash, - }, - ) - - if return_status == VerificationStatus.VERIFIED: - return synchronized_data, Event.DONE - if ( - return_status == VerificationStatus.NOT_VERIFIED - and cast( - SynchronizedData, self.synchronized_data - ).should_check_late_messages - ): - return synchronized_data, Event.CHECK_LATE_ARRIVING_MESSAGE - if return_status == VerificationStatus.NOT_VERIFIED: - return synchronized_data, Event.NEGATIVE - if return_status == VerificationStatus.BAD_SAFE_NONCE: - # in case a bad nonce was used, we need to recreate the tx from scratch - return synchronized_data, Event.NONE - - return synchronized_data, Event.NONE - - if not self.is_majority_possible( - self.collection, self.synchronized_data.nb_participants - ): - return self.synchronized_data, Event.NO_MAJORITY - return None - - -class CheckLateTxHashesRound(CheckTransactionHistoryRound): - """A round in which agents check the late-arriving transaction hashes to see if any of them has been validated""" - - -class SynchronizeLateMessagesRound(CollectNonEmptyUntilThresholdRound): - """A round in which agents synchronize potentially late arriving messages""" - - payload_class = SynchronizeLateMessagesPayload - synchronized_data_class = SynchronizedData - done_event = Event.DONE - none_event = Event.NONE - required_block_confirmations = 3 - selection_key = get_name(SynchronizedData.late_arriving_tx_hashes) - collection_key = get_name(SynchronizedData.participant_to_late_messages) - # if the payload is serialized to bytes, we verify that the length specified matches - _hash_length = TX_HASH_LENGTH - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: - """Process the end of the block.""" - result = super().end_block() - if result is None: - return None - - synchronized_data, event = cast(Tuple[SynchronizedData, Event], result) - - late_arriving_tx_hashes_counts = { - sender: len(hashes) - for sender, hashes in synchronized_data.late_arriving_tx_hashes.items() - } - missed_after_sync = { - sender: missed - late_arriving_tx_hashes_counts.get(sender, 0) - for sender, missed in synchronized_data.missed_messages.items() - } - suspects = tuple(filter_negative(missed_after_sync)) - - if suspects: - synchronized_data = cast( - SynchronizedData, - synchronized_data.update( - synchronized_data_class=self.synchronized_data_class, - **{get_name(SynchronizedData.suspects): suspects}, - ), - ) - return synchronized_data, Event.SUSPICIOUS_ACTIVITY - - synchronized_data = cast( - SynchronizedData, - synchronized_data.update( - synchronized_data_class=self.synchronized_data_class, - **{get_name(SynchronizedData.missed_messages): missed_after_sync}, - ), - ) - return synchronized_data, event - - def process_payload(self, payload: BaseTxPayload) -> None: - """Process payload.""" - # TODO: move check into payload definition via `post_init` - payload = cast(SynchronizeLateMessagesPayload, payload) - if self._hash_length: - content = payload.tx_hashes - if not content or len(content) % self._hash_length: - msg = f"Expecting serialized data of chunk size {self._hash_length}" - raise ABCIAppInternalError(f"{msg}, got: {content} in {self.round_id}") - super().process_payload(payload) - - def check_payload(self, payload: BaseTxPayload) -> None: - """Check Payload""" - # TODO: move check into payload definition via `post_init` - payload = cast(SynchronizeLateMessagesPayload, payload) - if self._hash_length: - content = payload.tx_hashes - if not content or len(content) % self._hash_length: - msg = f"Expecting serialized data of chunk size {self._hash_length}" - raise TransactionNotValidError( - f"{msg}, got: {content} in {self.round_id}" - ) - super().check_payload(payload) - - -class FinishedTransactionSubmissionRound(DegenerateRound, ABC): - """A round that represents the transition to the ResetAndPauseRound""" - - -class ResetRound(CollectSameUntilThresholdRound): - """A round that represents the reset of a period""" - - payload_class = ResetPayload - synchronized_data_class = SynchronizedData - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: - """Process the end of the block.""" - if self.threshold_reached: - synchronized_data = cast(SynchronizedData, self.synchronized_data) - # we could have used the `synchronized_data.create()` here and set the `cross_period_persisted_keys` - # with the corresponding properties' keys. However, the cross period keys would get passed over - # for all the following periods, even those that the tx settlement succeeds. - # Therefore, we need to manually call the db's create method and pass the keys we want to keep only - # for the next period, which comes after a `NO_MAJORITY` event of the tx settlement skill. - # TODO investigate the following: - # This probably indicates an issue with the logic of this skill. We should not increase the period since - # we have a failure. We could instead just remove the `ResetRound` and transition to the - # `RandomnessTransactionSubmissionRound` directly. This would save us one round, would allow us to remove - # this hacky logic for the `create`, and would also not increase the period count in non-successful events - self.synchronized_data.db.create( - **{ - db_key: synchronized_data.db.get(db_key, default) - for db_key, default in { - "all_participants": VALUE_NOT_PROVIDED, - "participants": VALUE_NOT_PROVIDED, - "consensus_threshold": VALUE_NOT_PROVIDED, - "safe_contract_address": VALUE_NOT_PROVIDED, - "tx_hashes_history": "", - "keepers": VALUE_NOT_PROVIDED, - "missed_messages": dict.fromkeys( - synchronized_data.all_participants, 0 - ), - "late_arriving_tx_hashes": VALUE_NOT_PROVIDED, - "suspects": tuple(), - }.items() - } - ) - return self.synchronized_data, Event.DONE - if not self.is_majority_possible( - self.collection, self.synchronized_data.nb_participants - ): - return self.synchronized_data, Event.NO_MAJORITY - return None - - -class TransactionSubmissionAbciApp(AbciApp[Event]): - """TransactionSubmissionAbciApp - - Initial round: RandomnessTransactionSubmissionRound - - Initial states: {RandomnessTransactionSubmissionRound} - - Transition states: - 0. RandomnessTransactionSubmissionRound - - done: 1. - - round timeout: 0. - - no majority: 0. - 1. SelectKeeperTransactionSubmissionARound - - done: 2. - - round timeout: 1. - - no majority: 10. - - incorrect serialization: 12. - 2. CollectSignatureRound - - done: 3. - - round timeout: 2. - - no majority: 10. - 3. FinalizationRound - - done: 4. - - check history: 5. - - finalize timeout: 7. - - finalization failed: 6. - - check late arriving message: 8. - - insufficient funds: 6. - 4. ValidateTransactionRound - - done: 11. - - negative: 5. - - none: 6. - - validate timeout: 5. - - no majority: 4. - 5. CheckTransactionHistoryRound - - done: 11. - - negative: 6. - - none: 12. - - check timeout: 5. - - no majority: 5. - - check late arriving message: 8. - 6. SelectKeeperTransactionSubmissionBRound - - done: 3. - - round timeout: 6. - - no majority: 10. - - incorrect serialization: 12. - 7. SelectKeeperTransactionSubmissionBAfterTimeoutRound - - done: 3. - - check history: 5. - - check late arriving message: 8. - - round timeout: 7. - - no majority: 10. - - incorrect serialization: 12. - 8. SynchronizeLateMessagesRound - - done: 9. - - round timeout: 8. - - none: 6. - - suspicious activity: 12. - 9. CheckLateTxHashesRound - - done: 11. - - negative: 12. - - none: 12. - - check timeout: 9. - - no majority: 12. - - check late arriving message: 8. - 10. ResetRound - - done: 0. - - reset timeout: 12. - - no majority: 12. - 11. FinishedTransactionSubmissionRound - 12. FailedRound - - Final states: {FailedRound, FinishedTransactionSubmissionRound} - - Timeouts: - round timeout: 30.0 - finalize timeout: 30.0 - validate timeout: 30.0 - check timeout: 30.0 - reset timeout: 30.0 - """ - - initial_round_cls: AppState = RandomnessTransactionSubmissionRound - initial_states: Set[AppState] = {RandomnessTransactionSubmissionRound} - transition_function: AbciAppTransitionFunction = { - RandomnessTransactionSubmissionRound: { - Event.DONE: SelectKeeperTransactionSubmissionARound, - Event.ROUND_TIMEOUT: RandomnessTransactionSubmissionRound, - Event.NO_MAJORITY: RandomnessTransactionSubmissionRound, - }, - SelectKeeperTransactionSubmissionARound: { - Event.DONE: CollectSignatureRound, - Event.ROUND_TIMEOUT: SelectKeeperTransactionSubmissionARound, - Event.NO_MAJORITY: ResetRound, - Event.INCORRECT_SERIALIZATION: FailedRound, - }, - CollectSignatureRound: { - Event.DONE: FinalizationRound, - Event.ROUND_TIMEOUT: CollectSignatureRound, - Event.NO_MAJORITY: ResetRound, - }, - FinalizationRound: { - Event.DONE: ValidateTransactionRound, - Event.CHECK_HISTORY: CheckTransactionHistoryRound, - Event.FINALIZE_TIMEOUT: SelectKeeperTransactionSubmissionBAfterTimeoutRound, - Event.FINALIZATION_FAILED: SelectKeeperTransactionSubmissionBRound, - Event.CHECK_LATE_ARRIVING_MESSAGE: SynchronizeLateMessagesRound, - Event.INSUFFICIENT_FUNDS: SelectKeeperTransactionSubmissionBRound, - }, - ValidateTransactionRound: { - Event.DONE: FinishedTransactionSubmissionRound, - Event.NEGATIVE: CheckTransactionHistoryRound, - Event.NONE: SelectKeeperTransactionSubmissionBRound, - # even in case of timeout we might've sent the transaction - # so we need to check the history - Event.VALIDATE_TIMEOUT: CheckTransactionHistoryRound, - Event.NO_MAJORITY: ValidateTransactionRound, - }, - CheckTransactionHistoryRound: { - Event.DONE: FinishedTransactionSubmissionRound, - Event.NEGATIVE: SelectKeeperTransactionSubmissionBRound, - Event.NONE: FailedRound, - Event.CHECK_TIMEOUT: CheckTransactionHistoryRound, - Event.NO_MAJORITY: CheckTransactionHistoryRound, - Event.CHECK_LATE_ARRIVING_MESSAGE: SynchronizeLateMessagesRound, - }, - SelectKeeperTransactionSubmissionBRound: { - Event.DONE: FinalizationRound, - Event.ROUND_TIMEOUT: SelectKeeperTransactionSubmissionBRound, - Event.NO_MAJORITY: ResetRound, - Event.INCORRECT_SERIALIZATION: FailedRound, - }, - SelectKeeperTransactionSubmissionBAfterTimeoutRound: { - Event.DONE: FinalizationRound, - Event.CHECK_HISTORY: CheckTransactionHistoryRound, - Event.CHECK_LATE_ARRIVING_MESSAGE: SynchronizeLateMessagesRound, - Event.ROUND_TIMEOUT: SelectKeeperTransactionSubmissionBAfterTimeoutRound, - Event.NO_MAJORITY: ResetRound, - Event.INCORRECT_SERIALIZATION: FailedRound, - }, - SynchronizeLateMessagesRound: { - Event.DONE: CheckLateTxHashesRound, - Event.ROUND_TIMEOUT: SynchronizeLateMessagesRound, - Event.NONE: SelectKeeperTransactionSubmissionBRound, - Event.SUSPICIOUS_ACTIVITY: FailedRound, - }, - CheckLateTxHashesRound: { - Event.DONE: FinishedTransactionSubmissionRound, - Event.NEGATIVE: FailedRound, - Event.NONE: FailedRound, - Event.CHECK_TIMEOUT: CheckLateTxHashesRound, - Event.NO_MAJORITY: FailedRound, - Event.CHECK_LATE_ARRIVING_MESSAGE: SynchronizeLateMessagesRound, - }, - ResetRound: { - Event.DONE: RandomnessTransactionSubmissionRound, - Event.RESET_TIMEOUT: FailedRound, - Event.NO_MAJORITY: FailedRound, - }, - FinishedTransactionSubmissionRound: {}, - FailedRound: {}, - } - final_states: Set[AppState] = { - FinishedTransactionSubmissionRound, - FailedRound, - } - event_to_timeout: Dict[Event, float] = { - Event.ROUND_TIMEOUT: 30.0, - Event.FINALIZE_TIMEOUT: 30.0, - Event.VALIDATE_TIMEOUT: 30.0, - Event.CHECK_TIMEOUT: 30.0, - Event.RESET_TIMEOUT: 30.0, - } - db_pre_conditions: Dict[AppState, Set[str]] = { - RandomnessTransactionSubmissionRound: { - get_name(SynchronizedData.most_voted_tx_hash), - get_name(SynchronizedData.participants), - } - } - db_post_conditions: Dict[AppState, Set[str]] = { - FinishedTransactionSubmissionRound: { - get_name(SynchronizedData.final_tx_hash), - get_name(SynchronizedData.final_verification_status), - }, - FailedRound: set(), - } diff --git a/packages/valory/skills/transaction_settlement_abci/skill.yaml b/packages/valory/skills/transaction_settlement_abci/skill.yaml deleted file mode 100644 index 61a9fab..0000000 --- a/packages/valory/skills/transaction_settlement_abci/skill.yaml +++ /dev/null @@ -1,174 +0,0 @@ -name: transaction_settlement_abci -author: valory -version: 0.1.0 -type: skill -description: ABCI application for transaction settlement. -license: Apache-2.0 -aea_version: '>=1.0.0, <2.0.0' -fingerprint: - README.md: bafybeihvqvbj2tiiyimz3e27gqhb7ku5rut7hycfahi4qle732kvj5fs7q - __init__.py: bafybeicyrp6x2efg43gfdekxuofrlidc3w6aubzmyioqwnryropp6u7sby - behaviours.py: bafybeibv5y34bwaloj455bjws3a2aeh2bgi4dclq2f5d35apm2mloen5xa - dialogues.py: bafybeigabhaykiyzbluu4mk6bbrmqhzld2kyp32pg24bvjmzrrb74einwm - fsm_specification.yaml: bafybeigdj64py4zjihcxdkvtrydbxyeh4slr2kkghltz3upnupdgad4et4 - handlers.py: bafybeie42qa3csgy6oompuqs2qnkat5mnslepbbwmgoxv6ljme4jofa5pe - models.py: bafybeiguxishqvtvlyznok3xjnzm4t6vfflamcvz5vtecq5esbldsxuc5e - payload_tools.py: bafybeiatlbw3vyo5ppjhxf4psdvkwubmrjolsprf44lis5ozfkjo7o3cba - payloads.py: bafybeiclhjnsgylqzfnu2azlqxor3vyldaoof757dnfwz5xbwejk2ro2cm - rounds.py: bafybeieo5l6gh276hhtztphloyknb5ew66hvqhzzjiv26isaz7ptvtqjgu - test_tools/__init__.py: bafybeibj2blgxzvcgdi5gzcnlzs2nt7bpdifzvjjlxlrkeutjy2qrqbwau - test_tools/integration.py: bafybeictb7ym4xsbo3ti5y2a2fpg344graa4d7352oozsea5rbab3kq4ae - tests/__init__.py: bafybeifukcwmf2ewkjqdu7j6xzmaovgrul7jnea5lrl4o3ianoofje6vfa - tests/test_behaviours.py: bafybeia2vob5legv3tdrdj4gjgmnz6enhaterbetkc6ntdemnwgg5or4gq - tests/test_dialogues.py: bafybeictrjf6jzsj4y6u2ftdrb2nyriiipia5b7wc4fsli3lwbjpd3mbam - tests/test_handlers.py: bafybeievntkwacpfaom3qabvrlworjqyd4sgfjknjlhys7f5tuq7725xli - tests/test_models.py: bafybeihvrv7vtaei64nv7okkfz2gg2g4ey4nei27ayc74h5bdlqpbk4xde - tests/test_payload_tools.py: bafybeihmgkcrlqhz4ncak276lnccmilig6gx3crmn33n46jcco6g5pzrje - tests/test_payloads.py: bafybeidvjqvjvnuw5vt4zgnqwzopvprznmefosqy3wcxukvobaiishygze - tests/test_rounds.py: bafybeic3kzqy3pe6d4skntnfc5443y6dshcustiuv2d6cw4z56gw2ewehy - tests/test_tools/__init__.py: bafybeiaq2ftmklvu5vqq6vdfa7mrlmrnusluki35jm5n2yzf57ox5dif74 - tests/test_tools/test_integration.py: bafybeigv6fxogm3aq3extahr75owdqnzepouv3rtxl3m4gai2urtz6u4ea -fingerprint_ignore_patterns: [] -connections: [] -contracts: -- valory/gnosis_safe:0.1.0:bafybeiho6sbfts3zk3mftrngw37d5qnlvkqtnttt3fzexmcwkeevhu4wwi -protocols: -- open_aea/signing:1.0.0:bafybeihv62fim3wl2bayavfcg3u5e5cxu3b7brtu4cn5xoxd6lqwachasi -- valory/abci:0.1.0:bafybeiaqmp7kocbfdboksayeqhkbrynvlfzsx4uy4x6nohywnmaig4an7u -- valory/contract_api:1.0.0:bafybeidgu7o5llh26xp3u3ebq3yluull5lupiyeu6iooi2xyymdrgnzq5i -- valory/ledger_api:1.0.0:bafybeihdk6psr4guxmbcrc26jr2cbgzpd5aljkqvpwo64bvaz7tdti2oni -skills: -- valory/abstract_round_abci:0.1.0:bafybeibovsktd3uxur45nrcomq5shcn46cgxd5idmhxbmjhg32c5abyqim -behaviours: - main: - args: {} - class_name: TransactionSettlementRoundBehaviour -handlers: - abci: - args: {} - class_name: ABCIHandler - contract_api: - args: {} - class_name: ContractApiHandler - http: - args: {} - class_name: HttpHandler - ipfs: - args: {} - class_name: IpfsHandler - ledger_api: - args: {} - class_name: LedgerApiHandler - signing: - args: {} - class_name: SigningHandler - tendermint: - args: {} - class_name: TendermintHandler -models: - abci_dialogues: - args: {} - class_name: AbciDialogues - benchmark_tool: - args: - log_dir: /logs - class_name: BenchmarkTool - contract_api_dialogues: - args: {} - class_name: ContractApiDialogues - http_dialogues: - args: {} - class_name: HttpDialogues - ipfs_dialogues: - args: {} - class_name: IpfsDialogues - ledger_api_dialogues: - args: {} - class_name: LedgerApiDialogues - params: - args: - cleanup_history_depth: 1 - cleanup_history_depth_current: null - default_chain_id: ethereum - drand_public_key: 868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31 - finalize_timeout: 60.0 - genesis_config: - genesis_time: '2022-05-20T16:00:21.735122717Z' - chain_id: chain-c4daS1 - consensus_params: - block: - max_bytes: '22020096' - max_gas: '-1' - time_iota_ms: '1000' - evidence: - max_age_num_blocks: '100000' - max_age_duration: '172800000000000' - max_bytes: '1048576' - validator: - pub_key_types: - - ed25519 - version: {} - voting_power: '10' - history_check_timeout: 1205 - init_fallback_gas: 0 - keeper_allowed_retries: 3 - keeper_timeout: 30.0 - light_slash_unit_amount: 5000000000000000 - max_attempts: 10 - max_healthcheck: 120 - on_chain_service_id: null - request_retry_delay: 1.0 - request_timeout: 10.0 - reset_pause_duration: 10 - reset_tendermint_after: 2 - retry_attempts: 400 - retry_timeout: 3 - round_timeout_seconds: 30.0 - serious_slash_unit_amount: 8000000000000000 - service_id: registration - service_registry_address: null - setup: {} - share_tm_config_on_startup: false - slash_cooldown_hours: 3 - slash_threshold_amount: 10000000000000000 - sleep_time: 1 - tendermint_check_sleep_delay: 3 - tendermint_com_url: http://localhost:8080 - tendermint_max_retries: 5 - tendermint_p2p_url: localhost:26656 - tendermint_url: http://localhost:26657 - tx_timeout: 10.0 - use_slashing: false - use_termination: false - validate_timeout: 1205 - class_name: TransactionParams - randomness_api: - args: - api_id: cloudflare - headers: {} - method: GET - parameters: {} - response_key: null - response_type: dict - retries: 5 - url: https://drand.cloudflare.com/public/latest - class_name: RandomnessApi - requests: - args: {} - class_name: Requests - signing_dialogues: - args: {} - class_name: SigningDialogues - state: - args: {} - class_name: SharedState - tendermint_dialogues: - args: {} - class_name: TendermintDialogues -dependencies: - open-aea-test-autonomy: - version: ==0.15.2 - web3: - version: <7,>=6.0.0 -is_abstract: true -customs: [] diff --git a/packages/valory/skills/transaction_settlement_abci/test_tools/__init__.py b/packages/valory/skills/transaction_settlement_abci/test_tools/__init__.py deleted file mode 100644 index 55be4e3..0000000 --- a/packages/valory/skills/transaction_settlement_abci/test_tools/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Tests package for transaction_settlement_abci derived skills.""" diff --git a/packages/valory/skills/transaction_settlement_abci/test_tools/integration.py b/packages/valory/skills/transaction_settlement_abci/test_tools/integration.py deleted file mode 100644 index deb1c53..0000000 --- a/packages/valory/skills/transaction_settlement_abci/test_tools/integration.py +++ /dev/null @@ -1,338 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Integration tests for various transaction settlement skill's failure modes.""" - - -import binascii -import os -import tempfile -from abc import ABC -from math import ceil -from typing import Any, Dict, Union, cast - -from aea.crypto.base import Crypto -from aea.crypto.registries import make_crypto, make_ledger_api -from aea_ledger_ethereum import EthereumApi -from aea_test_autonomy.helpers.contracts import get_register_contract -from web3.types import Nonce, Wei - -from packages.open_aea.protocols.signing import SigningMessage -from packages.valory.contracts.gnosis_safe.tests.test_contract import ( - PACKAGE_DIR as GNOSIS_SAFE_PACKAGE, -) -from packages.valory.protocols.contract_api import ContractApiMessage -from packages.valory.protocols.contract_api.custom_types import RawTransaction, State -from packages.valory.protocols.ledger_api import LedgerApiMessage -from packages.valory.protocols.ledger_api.custom_types import ( - SignedTransaction, - TransactionDigest, - TransactionReceipt, -) -from packages.valory.skills.abstract_round_abci.test_tools.integration import ( - ExpectedContentType, - ExpectedTypesType, - HandlersType, - IntegrationBaseCase, -) -from packages.valory.skills.transaction_settlement_abci.behaviours import ( - FinalizeBehaviour, - ValidateTransactionBehaviour, -) -from packages.valory.skills.transaction_settlement_abci.payload_tools import ( - VerificationStatus, - skill_input_hex_to_payload, -) -from packages.valory.skills.transaction_settlement_abci.payloads import SignaturePayload -from packages.valory.skills.transaction_settlement_abci.rounds import ( - SynchronizedData as TxSettlementSynchronizedSata, -) - - -# pylint: disable=protected-access,too-many-ancestors,unbalanced-tuple-unpacking,too-many-locals,consider-using-with,unspecified-encoding,too-many-arguments,unidiomatic-typecheck - - -DUMMY_MAX_FEE_PER_GAS = 4000000000 -DUMMY_MAX_PRIORITY_FEE_PER_GAS = 3000000000 -DUMMY_REPRICING_MULTIPLIER = 1.1 - - -class _SafeConfiguredHelperIntegration(IntegrationBaseCase, ABC): # pragma: no cover - """Base test class for integration tests with Gnosis, but no contract, deployed.""" - - safe_owners: Dict[str, Crypto] - keeper_address: str - - @classmethod - def setup_class(cls, **kwargs: Any) -> None: - """Setup.""" - super().setup_class() - - # safe configuration - cls.safe_owners = {} - for address, p_key in cls.agents.items(): - with tempfile.TemporaryDirectory() as temp_dir: - fp = os.path.join(temp_dir, "key.txt") - f = open(fp, "w") - f.write(p_key) - f.close() - crypto = make_crypto("ethereum", private_key_path=str(fp)) - cls.safe_owners[address] = crypto - cls.keeper_address = cls.current_agent - assert cls.keeper_address in cls.safe_owners # nosec - - -class _GnosisHelperIntegration( - _SafeConfiguredHelperIntegration, ABC -): # pragma: no cover - """Class that assists Gnosis instantiation.""" - - safe_contract_address: str = "0x68FCdF52066CcE5612827E872c45767E5a1f6551" - ethereum_api: EthereumApi - gnosis_instance: Any - - @classmethod - def setup_class(cls, **kwargs: Any) -> None: - """Setup.""" - super().setup_class() - - # register gnosis contract - gnosis = get_register_contract(GNOSIS_SAFE_PACKAGE) - - cls.ethereum_api = make_ledger_api("ethereum") - cls.gnosis_instance = gnosis.get_instance( - cls.ethereum_api, cls.safe_contract_address - ) - - -class _TxHelperIntegration(_GnosisHelperIntegration, ABC): # pragma: no cover - """Class that assists tx settlement related operations.""" - - tx_settlement_synchronized_data: TxSettlementSynchronizedSata - - def sign_tx(self) -> None: - """Sign a transaction""" - tx_params = skill_input_hex_to_payload( - self.tx_settlement_synchronized_data.most_voted_tx_hash - ) - safe_tx_hash_bytes = binascii.unhexlify(tx_params["safe_tx_hash"]) - participant_to_signature = {} - for address, crypto in self.safe_owners.items(): - signature_hex = crypto.sign_message( - safe_tx_hash_bytes, - is_deprecated_mode=True, - ) - signature_hex = signature_hex[2:] - participant_to_signature[address] = SignaturePayload( - sender=address, - signature=signature_hex, - ).json - - # FIXME: The following loop is a patch. The - # [_get_python_modules](https://github.com/valory-xyz/open-aea/blob/d0e60881b1371442c3572df86c53fc92dc9228fa/aea/skills/base.py#L907-L925) - # is getting the python modules from the skill directory. - # As we can see from the code, the path will end up being relative, which means that the - # [_metaclass_registry_key](https://github.com/valory-xyz/open-autonomy/blob/5d151f1fff4934f70be8c5f6be77705cc2e6ef4c/packages/valory/skills/abstract_round_abci/base.py#L167) - # inserted in the `_MetaPayload`'s registry will also be relative. However, this is causing issues when calling - # `BaseTxPayload.from_json(payload_json)` later from the property below - # (`self.tx_settlement_synchronized_data.participant_to_signature`) because the `payload_json` will have been - # serialized using an imported payload (the `SignaturePayload` above), and therefore a key error will be - # raised since the imported payload's path is not relative and the registry has a relative path as a key. - for payload in participant_to_signature.values(): - registry_key = "_metaclass_registry_key" - payload_value = payload[registry_key] - payload_cls_name = payload_value.split(".")[-1] - patched_registry_key = f"payloads.{payload_cls_name}" - payload[registry_key] = patched_registry_key - - self.tx_settlement_synchronized_data.update( - participant_to_signature=participant_to_signature, - ) - - actual_safe_owners = self.gnosis_instance.functions.getOwners().call() - expected_safe_owners = ( - self.tx_settlement_synchronized_data.participant_to_signature.keys() - ) - assert len(actual_safe_owners) == len(expected_safe_owners) # nosec - assert all( # nosec - owner == signer - for owner, signer in zip(actual_safe_owners, expected_safe_owners) - ) - - def send_tx(self, simulate_timeout: bool = False) -> None: - """Send a transaction""" - - self.fast_forward_to_behaviour( - behaviour=self.behaviour, - behaviour_id=FinalizeBehaviour.auto_behaviour_id(), - synchronized_data=self.tx_settlement_synchronized_data, - ) - behaviour = cast(FinalizeBehaviour, self.behaviour.current_behaviour) - assert behaviour.behaviour_id == FinalizeBehaviour.auto_behaviour_id() - stored_nonce = behaviour.params.mutable_params.nonce - stored_gas_price = behaviour.params.mutable_params.gas_price - - handlers: HandlersType = [ - self.contract_handler, - self.signing_handler, - self.ledger_handler, - ] - expected_content: ExpectedContentType = [ - {"performative": ContractApiMessage.Performative.RAW_TRANSACTION}, - {"performative": SigningMessage.Performative.SIGNED_TRANSACTION}, - {"performative": LedgerApiMessage.Performative.TRANSACTION_DIGEST}, - ] - expected_types: ExpectedTypesType = [ - { - "raw_transaction": RawTransaction, - }, - { - "signed_transaction": SignedTransaction, - }, - { - "transaction_digest": TransactionDigest, - }, - ] - msg1, _, msg3 = self.process_n_messages( - 3, - self.tx_settlement_synchronized_data, - None, - handlers, - expected_content, - expected_types, - fail_send_a2a=simulate_timeout, - ) - assert msg1 is not None and isinstance(msg1, ContractApiMessage) # nosec - assert msg3 is not None and isinstance(msg3, LedgerApiMessage) # nosec - nonce_used = Nonce(int(cast(str, msg1.raw_transaction.body["nonce"]))) - gas_price_used = { - gas_price_param: Wei( - int( - cast( - str, - msg1.raw_transaction.body[gas_price_param], - ) - ) - ) - for gas_price_param in ("maxPriorityFeePerGas", "maxFeePerGas") - } - tx_digest = msg3.transaction_digest.body - tx_data = { - "status": VerificationStatus.PENDING, - "tx_digest": cast(str, tx_digest), - } - - behaviour = cast(FinalizeBehaviour, self.behaviour.current_behaviour) - assert behaviour.params.mutable_params.gas_price == gas_price_used # nosec - assert behaviour.params.mutable_params.nonce == nonce_used # nosec - if simulate_timeout: - assert behaviour.params.mutable_params.tx_hash == tx_digest # nosec - else: - assert behaviour.params.mutable_params.tx_hash == "" # nosec - - # if we are repricing - if nonce_used == stored_nonce: - assert stored_nonce is not None # nosec - assert stored_gas_price is not None # nosec - assert gas_price_used == { # nosec - gas_price_param: ceil( - stored_gas_price[gas_price_param] * DUMMY_REPRICING_MULTIPLIER - ) - for gas_price_param in ("maxPriorityFeePerGas", "maxFeePerGas") - }, "The repriced parameters do not match the ones returned from the gas pricing method!" - # if we are not repricing - else: - assert gas_price_used == { # nosec - "maxPriorityFeePerGas": DUMMY_MAX_PRIORITY_FEE_PER_GAS, - "maxFeePerGas": DUMMY_MAX_FEE_PER_GAS, - }, "The used parameters do not match the ones returned from the gas pricing method!" - - update_params: Dict[str, Union[int, str, Dict[str, int]]] - if not simulate_timeout: - hashes = self.tx_settlement_synchronized_data.tx_hashes_history - hashes.append(tx_digest) - update_params = dict( - tx_hashes_history="".join(hashes), - final_verification_status=VerificationStatus(tx_data["status"]).value, - ) - else: - # store the tx hash that we have missed and update missed messages. - assert isinstance( # nosec - self.behaviour.current_behaviour, FinalizeBehaviour - ) - self.mock_a2a_transaction() - self.behaviour.current_behaviour.params.mutable_params.tx_hash = tx_digest - missed_messages = self.tx_settlement_synchronized_data.missed_messages - missed_messages[ - self.tx_settlement_synchronized_data.most_voted_keeper_address - ] += 1 - update_params = dict(missed_messages=missed_messages) - - self.tx_settlement_synchronized_data.update( - synchronized_data_class=None, **update_params - ) - - def validate_tx( - self, simulate_timeout: bool = False, mining_interval_secs: float = 0 - ) -> None: - """Validate the sent transaction.""" - - if simulate_timeout: - missed_messages = self.tx_settlement_synchronized_data.missed_messages - missed_messages[ - tuple(self.tx_settlement_synchronized_data.all_participants)[0] - ] += 1 - self.tx_settlement_synchronized_data.update(missed_messages=missed_messages) - else: - handlers: HandlersType = [ - self.ledger_handler, - self.contract_handler, - ] - expected_content: ExpectedContentType = [ - {"performative": LedgerApiMessage.Performative.TRANSACTION_RECEIPT}, - {"performative": ContractApiMessage.Performative.STATE}, - ] - expected_types: ExpectedTypesType = [ - { - "transaction_receipt": TransactionReceipt, - }, - { - "state": State, - }, - ] - _, verif_msg = self.process_n_messages( - 2, - self.tx_settlement_synchronized_data, - ValidateTransactionBehaviour.auto_behaviour_id(), - handlers, - expected_content, - expected_types, - mining_interval_secs=mining_interval_secs, - ) - assert verif_msg is not None and isinstance( # nosec - verif_msg, ContractApiMessage - ) - assert verif_msg.state.body[ # nosec - "verified" - ], f"Message not verified: {verif_msg.state.body}" - - self.tx_settlement_synchronized_data.update( - final_verification_status=VerificationStatus.VERIFIED.value, - final_tx_hash=self.tx_settlement_synchronized_data.to_be_validated_tx_hash, - ) diff --git a/packages/valory/skills/transaction_settlement_abci/tests/__init__.py b/packages/valory/skills/transaction_settlement_abci/tests/__init__.py deleted file mode 100644 index 932ecb0..0000000 --- a/packages/valory/skills/transaction_settlement_abci/tests/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Tests for valory/transaction_settlement_abci skill.""" -from pathlib import Path - - -PACKAGE_DIR = Path(__file__).parents[1] diff --git a/packages/valory/skills/transaction_settlement_abci/tests/test_behaviours.py b/packages/valory/skills/transaction_settlement_abci/tests/test_behaviours.py deleted file mode 100644 index fbc6d9f..0000000 --- a/packages/valory/skills/transaction_settlement_abci/tests/test_behaviours.py +++ /dev/null @@ -1,1392 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Tests for valory/registration_abci skill's behaviours.""" - -# pylint: skip-file - -import logging -import time -from collections import deque -from pathlib import Path -from typing import ( - Any, - Callable, - Deque, - Dict, - Generator, - List, - Optional, - Set, - Tuple, - Type, - Union, - cast, -) -from unittest import mock -from unittest.mock import MagicMock - -import pytest -from _pytest.logging import LogCaptureFixture -from _pytest.monkeypatch import MonkeyPatch -from aea.helpers.transaction.base import ( - RawTransaction, - SignedMessage, - SignedTransaction, -) -from aea.helpers.transaction.base import State as TrState -from aea.helpers.transaction.base import TransactionDigest, TransactionReceipt -from aea.skills.base import SkillContext -from web3.types import Nonce - -from packages.open_aea.protocols.signing import SigningMessage -from packages.valory.contracts.gnosis_safe.contract import ( - PUBLIC_ID as GNOSIS_SAFE_CONTRACT_ID, -) -from packages.valory.protocols.abci import AbciMessage # noqa: F401 -from packages.valory.protocols.contract_api.message import ContractApiMessage -from packages.valory.protocols.ledger_api.message import LedgerApiMessage -from packages.valory.skills.abstract_round_abci.base import AbciAppDB -from packages.valory.skills.abstract_round_abci.behaviour_utils import ( - BaseBehaviour, - RPCResponseStatus, - make_degenerate_behaviour, -) -from packages.valory.skills.abstract_round_abci.test_tools.base import ( - FSMBehaviourBaseCase, -) -from packages.valory.skills.abstract_round_abci.test_tools.common import ( - BaseRandomnessBehaviourTest, - BaseSelectKeeperBehaviourTest, -) -from packages.valory.skills.transaction_settlement_abci import PUBLIC_ID -from packages.valory.skills.transaction_settlement_abci.behaviours import ( - CheckLateTxHashesBehaviour, - CheckTransactionHistoryBehaviour, - FinalizeBehaviour, - REVERT_CODES_TO_REASONS, - RandomnessTransactionSubmissionBehaviour, - ResetBehaviour, - SelectKeeperTransactionSubmissionBehaviourA, - SelectKeeperTransactionSubmissionBehaviourB, - SignatureBehaviour, - SynchronizeLateMessagesBehaviour, - TransactionSettlementBaseBehaviour, - TxDataType, - ValidateTransactionBehaviour, -) -from packages.valory.skills.transaction_settlement_abci.payload_tools import ( - VerificationStatus, - hash_payload_to_hex, -) -from packages.valory.skills.transaction_settlement_abci.rounds import ( - Event as TransactionSettlementEvent, -) -from packages.valory.skills.transaction_settlement_abci.rounds import ( - FinishedTransactionSubmissionRound, -) -from packages.valory.skills.transaction_settlement_abci.rounds import ( - SynchronizedData as TransactionSettlementSynchronizedSata, -) - - -PACKAGE_DIR = Path(__file__).parent.parent - - -def mock_yield_and_return( - return_value: Any, -) -> Callable[[], Generator[None, None, Any]]: - """Wrapper for a Dummy generator that returns a `bool`.""" - - def yield_and_return(*_: Any, **__: Any) -> Generator[None, None, Any]: - """Dummy generator that returns a `bool`.""" - yield - return return_value - - return yield_and_return - - -def test_skill_public_id() -> None: - """Test skill module public ID""" - - assert PUBLIC_ID.name == Path(__file__).parents[1].name - assert PUBLIC_ID.author == Path(__file__).parents[3].name - - -class TransactionSettlementFSMBehaviourBaseCase(FSMBehaviourBaseCase): - """Base case for testing TransactionSettlement FSMBehaviour.""" - - path_to_skill = PACKAGE_DIR - - def ffw_signature(self, db_items: Optional[Dict] = None) -> None: - """Fast-forward to the `SignatureBehaviour`.""" - if db_items is None: - db_items = {} - - self.fast_forward_to_behaviour( - behaviour=self.behaviour, - behaviour_id=SignatureBehaviour.auto_behaviour_id(), - synchronized_data=TransactionSettlementSynchronizedSata( - AbciAppDB( - setup_data=AbciAppDB.data_to_lists(db_items), - ) - ), - ) - - -class TestTransactionSettlementBaseBehaviour(TransactionSettlementFSMBehaviourBaseCase): - """Test `TransactionSettlementBaseBehaviour`.""" - - @pytest.mark.parametrize( - "message, tx_digest, rpc_status, expected_data, replacement", - ( - ( - MagicMock( - performative=ContractApiMessage.Performative.ERROR, message="GS026" - ), - None, - RPCResponseStatus.SUCCESS, - { - "blacklisted_keepers": set(), - "keeper_retries": 2, - "keepers": deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35)), - "status": VerificationStatus.VERIFIED, - "tx_digest": "", - }, - False, - ), - ( - MagicMock( - performative=ContractApiMessage.Performative.ERROR, message="test" - ), - None, - RPCResponseStatus.SUCCESS, - { - "blacklisted_keepers": set(), - "keeper_retries": 2, - "keepers": deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35)), - "status": VerificationStatus.ERROR, - "tx_digest": "", - }, - False, - ), - ( - MagicMock(performative=ContractApiMessage.Performative.RAW_MESSAGE), - None, - RPCResponseStatus.SUCCESS, - { - "blacklisted_keepers": set(), - "keeper_retries": 2, - "keepers": deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35)), - "status": VerificationStatus.PENDING, - "tx_digest": "", - }, - False, - ), - ( - MagicMock(performative=ContractApiMessage.Performative.RAW_TRANSACTION), - None, - RPCResponseStatus.INCORRECT_NONCE, - { - "blacklisted_keepers": set(), - "keeper_retries": 2, - "keepers": deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35)), - "status": VerificationStatus.ERROR, - "tx_digest": "", - }, - False, - ), - ( - MagicMock(performative=ContractApiMessage.Performative.RAW_TRANSACTION), - None, - RPCResponseStatus.INSUFFICIENT_FUNDS, - { - "blacklisted_keepers": {"agent_1" + "-" * 35}, - "keeper_retries": 1, - "keepers": deque(("agent_3" + "-" * 35,)), - "status": VerificationStatus.INSUFFICIENT_FUNDS, - "tx_digest": "", - }, - False, - ), - ( - MagicMock(performative=ContractApiMessage.Performative.RAW_TRANSACTION), - None, - RPCResponseStatus.UNCLASSIFIED_ERROR, - { - "blacklisted_keepers": set(), - "keeper_retries": 2, - "keepers": deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35)), - "status": VerificationStatus.PENDING, - "tx_digest": "", - }, - False, - ), - ( - MagicMock(performative=ContractApiMessage.Performative.RAW_TRANSACTION), - None, - RPCResponseStatus.UNDERPRICED, - { - "blacklisted_keepers": set(), - "keeper_retries": 2, - "keepers": deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35)), - "status": VerificationStatus.PENDING, - "tx_digest": "", - }, - False, - ), - ( - MagicMock(performative=ContractApiMessage.Performative.RAW_TRANSACTION), - "test_digest_0", - RPCResponseStatus.ALREADY_KNOWN, - { - "blacklisted_keepers": set(), - "keeper_retries": 2, - "keepers": deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35)), - "status": VerificationStatus.PENDING, - "tx_digest": "test_digest_0", - }, - False, - ), - ( - MagicMock( - performative=ContractApiMessage.Performative.RAW_TRANSACTION, - raw_transaction=MagicMock( - body={ - "nonce": 0, - "maxPriorityFeePerGas": 10, - "maxFeePerGas": 20, - "gas": 0, - } - ), - ), - "test_digest_1", - RPCResponseStatus.SUCCESS, - { - "blacklisted_keepers": set(), - "keeper_retries": 2, - "keepers": deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35)), - "status": VerificationStatus.PENDING, - "tx_digest": "test_digest_1", - }, - False, - ), - ( - MagicMock( - performative=ContractApiMessage.Performative.RAW_TRANSACTION, - raw_transaction=MagicMock( - body={ - "nonce": 0, - "maxPriorityFeePerGas": 10, - "maxFeePerGas": 20, - "gas": 0, - } - ), - ), - "test_digest_2", - RPCResponseStatus.SUCCESS, - { - "blacklisted_keepers": set(), - "keeper_retries": 2, - "keepers": deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35)), - "status": VerificationStatus.PENDING, - "tx_digest": "test_digest_2", - }, - True, - ), - ), - ) - def test__get_tx_data( - self, - message: ContractApiMessage, - tx_digest: Optional[str], - rpc_status: RPCResponseStatus, - expected_data: TxDataType, - replacement: bool, - monkeypatch: MonkeyPatch, - ) -> None: - """Test `_get_tx_data`.""" - # fast-forward to any behaviour of the tx settlement skill - init_db_items = dict( - most_voted_tx_hash="b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d90000000" - "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - "0000000000000000000000002625a000x77E9b2EF921253A171Fa0CB9ba80558648Ff7215b0e6add595e00477c" - "f347d09797b156719dc5233283ac76e4efce2a674fe72d9b0e6add595e00477cf347d09797b156719dc5233283" - "ac76e4efce2a674fe72d9", - keepers=int(2).to_bytes(32, "big").hex() - + "".join(deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35))), - ) - self.ffw_signature(init_db_items) - behaviour = cast(SignatureBehaviour, self.behaviour.current_behaviour) - assert behaviour.behaviour_id == SignatureBehaviour.auto_behaviour_id() - # Set `nonce` to the same value as the returned, so that we test the tx replacement logging. - if replacement: - behaviour.params.mutable_params.nonce = Nonce(0) - - # patch the `send_raw_transaction` method - def dummy_send_raw_transaction( - *_: Any, **kwargs: Any - ) -> Generator[None, None, Tuple[Optional[str], RPCResponseStatus]]: - """Dummy `send_raw_transaction` method.""" - yield - return tx_digest, rpc_status - - monkeypatch.setattr( - BaseBehaviour, "send_raw_transaction", dummy_send_raw_transaction - ) - # call `_get_tx_data` - tx_data_iterator = cast( - TransactionSettlementBaseBehaviour, self.behaviour.current_behaviour - )._get_tx_data(message, use_flashbots=False) - - if message.performative == ContractApiMessage.Performative.RAW_TRANSACTION: - next(tx_data_iterator) - - try: - next(tx_data_iterator) - except StopIteration as res: - assert res.value == expected_data - - """Test the serialized_keepers method.""" - behaviour_ = self.behaviour.current_behaviour - assert behaviour_ is not None - assert behaviour_.serialized_keepers(deque([]), 1) == "" - assert ( - behaviour_.serialized_keepers(deque(["-" * 42]), 1) - == "0000000000000000000000000000000000000000000000000000000000000001" - "------------------------------------------" - ) - - @pytest.mark.parametrize( - argnames=["tx_body", "expected_params"], - argvalues=[ - [ - {"maxPriorityFeePerGas": "dummy", "maxFeePerGas": "dummy"}, - ["maxPriorityFeePerGas", "maxFeePerGas"], - ], - [{"gasPrice": "dummy"}, ["gasPrice"]], - [ - {"maxPriorityFeePerGas": "dummy"}, - [], - ], - [ - {"maxFeePerGas": "dummy"}, - [], - ], - [ - {}, - [], - ], - [ - { - "maxPriorityFeePerGas": "dummy", - "maxFeePerGas": "dummy", - "gasPrice": "dummy", - }, - ["maxPriorityFeePerGas", "maxFeePerGas"], - ], - ], - ) - def test_get_gas_price_params( - self, tx_body: dict, expected_params: List[str] - ) -> None: - """Test the get_gas_price_params method""" - # fast-forward to any behaviour of the tx settlement skill - self.ffw_signature() - - assert ( - cast( - TransactionSettlementBaseBehaviour, self.behaviour.current_behaviour - ).get_gas_price_params(tx_body) - == expected_params - ) - - def test_parse_revert_reason_successful(self) -> None: - """Test `_parse_revert_reason` method.""" - # fast-forward to any behaviour of the tx settlement skill - self.ffw_signature() - - for code, explanation in REVERT_CODES_TO_REASONS.items(): - message = MagicMock( - performative=ContractApiMessage.Performative.ERROR, - message=f"some text {code}.", - ) - - expected = f"Received a {code} revert error: {explanation}." - - assert ( - cast( - TransactionSettlementBaseBehaviour, self.behaviour.current_behaviour - )._parse_revert_reason(message) - == expected - ) - - @pytest.mark.parametrize( - "message", - ( - MagicMock( - performative=ContractApiMessage.Performative.ERROR, - message="Non existing code should be invalid GS086.", - ), - MagicMock( - performative=ContractApiMessage.Performative.ERROR, - message="Code not matching the regex should be invalid GS0265.", - ), - MagicMock( - performative=ContractApiMessage.Performative.ERROR, - message="No code in the message should be invalid.", - ), - MagicMock( - performative=ContractApiMessage.Performative.ERROR, - message="", # empty message should be invalid - ), - MagicMock( - performative=ContractApiMessage.Performative.ERROR, - message=None, # `None` message should be invalid - ), - ), - ) - def test_parse_revert_reason_unsuccessful( - self, message: ContractApiMessage - ) -> None: - """Test `_parse_revert_reason` method.""" - # fast-forward to any behaviour of the tx settlement skill - self.ffw_signature() - - expected = f"get_raw_safe_transaction unsuccessful! Received: {message}" - - assert ( - cast( - TransactionSettlementBaseBehaviour, self.behaviour.current_behaviour - )._parse_revert_reason(message) - == expected - ) - - -class TestRandomnessInOperation(BaseRandomnessBehaviourTest): - """Test randomness in operation.""" - - path_to_skill = PACKAGE_DIR - - randomness_behaviour_class = RandomnessTransactionSubmissionBehaviour - next_behaviour_class = SelectKeeperTransactionSubmissionBehaviourA - done_event = TransactionSettlementEvent.DONE - - -class TestSelectKeeperTransactionSubmissionBehaviourA(BaseSelectKeeperBehaviourTest): - """Test SelectKeeperBehaviour.""" - - path_to_skill = PACKAGE_DIR - - select_keeper_behaviour_class = SelectKeeperTransactionSubmissionBehaviourA - next_behaviour_class = SignatureBehaviour - done_event = TransactionSettlementEvent.DONE - _synchronized_data = TransactionSettlementSynchronizedSata - - -class TestSelectKeeperTransactionSubmissionBehaviourB( - TestSelectKeeperTransactionSubmissionBehaviourA -): - """Test SelectKeeperBehaviour.""" - - select_keeper_behaviour_class = SelectKeeperTransactionSubmissionBehaviourB - next_behaviour_class = FinalizeBehaviour - - @mock.patch.object( - TransactionSettlementSynchronizedSata, - "keepers", - new_callable=mock.PropertyMock, - ) - @mock.patch.object( - TransactionSettlementSynchronizedSata, - "keeper_retries", - new_callable=mock.PropertyMock, - ) - @mock.patch.object( - TransactionSettlementSynchronizedSata, - "final_verification_status", - new_callable=mock.PropertyMock, - ) - @pytest.mark.parametrize( - "keepers, keeper_retries, blacklisted_keepers, final_verification_status", - ( - ( - deque(f"keeper_{i}" for i in range(4)), - 1, - set(), - VerificationStatus.NOT_VERIFIED, - ), - (deque(("test_keeper",)), 2, set(), VerificationStatus.PENDING), - (deque(("test_keeper",)), 2, set(), VerificationStatus.NOT_VERIFIED), - (deque(("test_keeper",)), 2, {"a1"}, VerificationStatus.NOT_VERIFIED), - ( - deque(("test_keeper",)), - 2, - {"test_keeper"}, - VerificationStatus.NOT_VERIFIED, - ), - ( - deque(("test_keeper",)), - 2, - {"a_1", "a_2", "test_keeper"}, - VerificationStatus.NOT_VERIFIED, - ), - (deque(("test_keeper",)), 1, set(), VerificationStatus.NOT_VERIFIED), - (deque(("test_keeper",)), 3, set(), VerificationStatus.NOT_VERIFIED), - ), - ) - def test_select_keeper( - self, - final_verification_status_mock: mock.PropertyMock, - keeper_retries_mock: mock.PropertyMock, - keepers_mock: mock.PropertyMock, - keepers: Deque[str], - keeper_retries: int, - blacklisted_keepers: Set[str], - final_verification_status: VerificationStatus, - ) -> None: - """Test select keeper agent.""" - keepers_mock.return_value = keepers - keeper_retries_mock.return_value = keeper_retries - final_verification_status_mock.return_value = final_verification_status - super().test_select_keeper(blacklisted_keepers=blacklisted_keepers) - - @mock.patch.object( - TransactionSettlementSynchronizedSata, - "final_verification_status", - new_callable=mock.PropertyMock, - return_value=VerificationStatus.PENDING, - ) - @pytest.mark.skip # Needs to be investigated, fails in CI only. look at #1710 - def test_select_keeper_tx_pending( - self, _: mock.PropertyMock, caplog: LogCaptureFixture - ) -> None: - """Test select keeper while tx is pending""" - - with caplog.at_level(logging.INFO): - super().test_select_keeper(blacklisted_keepers=set()) - assert "Kept keepers and incremented retries" in caplog.text - - -class TestSignatureBehaviour(TransactionSettlementFSMBehaviourBaseCase): - """Test SignatureBehaviour.""" - - def test_signature_behaviour( - self, - ) -> None: - """Test signature behaviour.""" - - init_db_items = dict( - most_voted_tx_hash="b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d90000000" - "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - "0000000000000000000000002625a000x77E9b2EF921253A171Fa0CB9ba80558648Ff7215b0e6add595e00477c" - "f347d09797b156719dc5233283ac76e4efce2a674fe72d9b0e6add595e00477cf347d09797b156719dc5233283" - "ac76e4efce2a674fe72d9", - ) - self.ffw_signature(init_db_items) - - assert ( - cast( - BaseBehaviour, - cast(BaseBehaviour, self.behaviour.current_behaviour), - ).behaviour_id - == SignatureBehaviour.auto_behaviour_id() - ) - self.behaviour.act_wrapper() - self.mock_signing_request( - request_kwargs=dict( - performative=SigningMessage.Performative.SIGN_MESSAGE, - ), - response_kwargs=dict( - performative=SigningMessage.Performative.SIGNED_MESSAGE, - signed_message=SignedMessage( - ledger_id="ethereum", body="stub_signature" - ), - ), - ) - self.mock_a2a_transaction() - self._test_done_flag_set() - self.end_round(TransactionSettlementEvent.DONE) - behaviour = cast(BaseBehaviour, self.behaviour.current_behaviour) - assert behaviour.behaviour_id == FinalizeBehaviour.auto_behaviour_id() - - -class TestFinalizeBehaviour(TransactionSettlementFSMBehaviourBaseCase): - """Test FinalizeBehaviour.""" - - behaviour_class = FinalizeBehaviour - - def test_non_sender_act( - self, - ) -> None: - """Test finalize behaviour.""" - participants = (self.skill.skill_context.agent_address, "a_1", "a_2") - retries = 1 - self.fast_forward_to_behaviour( - behaviour=self.behaviour, - behaviour_id=self.behaviour_class.auto_behaviour_id(), - synchronized_data=TransactionSettlementSynchronizedSata( - AbciAppDB( - setup_data=AbciAppDB.data_to_lists( - dict( - most_voted_keeper_address="most_voted_keeper_address", - participants=participants, - # keeper needs to have length == 42 in order to be parsed - keepers=retries.to_bytes(32, "big").hex() - + "other_agent" - + "-" * 31, - ) - ), - ) - ), - ) - assert self.behaviour.current_behaviour is not None - assert ( - self.behaviour.current_behaviour.behaviour_id - == self.behaviour_class.auto_behaviour_id() - ) - cast( - FinalizeBehaviour, self.behaviour.current_behaviour - ).params.mutable_params.tx_hash = "test" - self.behaviour.act_wrapper() - self._test_done_flag_set() - self.end_round(TransactionSettlementEvent.DONE) - behaviour = cast(ValidateTransactionBehaviour, self.behaviour.current_behaviour) - assert ( - behaviour.behaviour_id == ValidateTransactionBehaviour.auto_behaviour_id() - ) - assert behaviour.params.mutable_params.tx_hash == "test" - - @pytest.mark.parametrize( - "resubmitting, response_kwargs", - ( - ( - ( - True, - dict( - performative=ContractApiMessage.Performative.RAW_TRANSACTION, - callable="get_deploy_transaction", - raw_transaction=RawTransaction( - ledger_id="ethereum", - body={ - "tx_hash": "0x3b", - "nonce": 0, - "maxFeePerGas": int(10e10), - "maxPriorityFeePerGas": int(10e10), - }, - ), - ), - ) - ), - ( - False, - dict( - performative=ContractApiMessage.Performative.RAW_TRANSACTION, - callable="get_deploy_transaction", - raw_transaction=RawTransaction( - ledger_id="ethereum", - body={ - "tx_hash": "0x3b", - "nonce": 0, - "maxFeePerGas": int(10e10), - "maxPriorityFeePerGas": int(10e10), - }, - ), - ), - ), - ( - False, - dict( - performative=ContractApiMessage.Performative.ERROR, - callable="get_deploy_transaction", - code=500, - message="GS026", - data=b"", - ), - ), - ( - False, - dict( - performative=ContractApiMessage.Performative.ERROR, - callable="get_deploy_transaction", - code=500, - message="other error", - data=b"", - ), - ), - ), - ) - @mock.patch.object(SkillContext, "agent_address", new_callable=mock.PropertyMock) - def test_sender_act( - self, - agent_address_mock: mock.PropertyMock, - resubmitting: bool, - response_kwargs: Dict[ - str, - Union[ - int, - str, - bytes, - Dict[str, Union[int, str]], - ContractApiMessage.Performative, - RawTransaction, - ], - ], - ) -> None: - """Test finalize behaviour.""" - nonce: Optional[int] = None - max_priority_fee_per_gas: Optional[int] = None - - if resubmitting: - nonce = 0 - max_priority_fee_per_gas = 1 - - # keepers need to have length == 42 in order to be parsed - agent_address_mock.return_value = "-" * 42 - retries = 1 - participants = ( - self.skill.skill_context.agent_address, - "a_1" + "-" * 39, - "a_2" + "-" * 39, - ) - self.fast_forward_to_behaviour( - behaviour=self.behaviour, - behaviour_id=self.behaviour_class.auto_behaviour_id(), - synchronized_data=TransactionSettlementSynchronizedSata( - AbciAppDB( - setup_data=AbciAppDB.data_to_lists( - dict( - safe_contract_address="safe_contract_address", - participants=participants, - participant_to_signature={}, - most_voted_tx_hash=hash_payload_to_hex( - "b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", - 1, - 1, - "0x77E9b2EF921253A171Fa0CB9ba80558648Ff7215", - b"b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", - ), - nonce=nonce, - max_priority_fee_per_gas=max_priority_fee_per_gas, - keepers=retries.to_bytes(32, "big").hex() - + self.skill.skill_context.agent_address, - ) - ), - ) - ), - ) - - assert self.behaviour.current_behaviour is not None - assert ( - self.behaviour.current_behaviour.behaviour_id - == self.behaviour_class.auto_behaviour_id() - ) - cast( - FinalizeBehaviour, self.behaviour.current_behaviour - ).params.mutable_params.tx_hash = "test" - self.behaviour.act_wrapper() - - self.mock_contract_api_request( - request_kwargs=dict( - performative=ContractApiMessage.Performative.GET_RAW_TRANSACTION, - ), - contract_id=str(GNOSIS_SAFE_CONTRACT_ID), - response_kwargs=response_kwargs, - ) - - if ( - response_kwargs["performative"] - == ContractApiMessage.Performative.RAW_TRANSACTION - ): - self.mock_signing_request( - request_kwargs=dict( - performative=SigningMessage.Performative.SIGN_TRANSACTION - ), - response_kwargs=dict( - performative=SigningMessage.Performative.SIGNED_TRANSACTION, - signed_transaction=SignedTransaction(ledger_id="ethereum", body={}), - ), - ) - self.mock_ledger_api_request( - request_kwargs=dict( - performative=LedgerApiMessage.Performative.SEND_SIGNED_TRANSACTION - ), - response_kwargs=dict( - performative=LedgerApiMessage.Performative.TRANSACTION_DIGEST, - transaction_digest=TransactionDigest( - ledger_id="ethereum", body="tx_hash" - ), - ), - ) - - self.mock_a2a_transaction() - self._test_done_flag_set() - self.end_round(TransactionSettlementEvent.DONE) - assert ( - self.behaviour.current_behaviour.behaviour_id - == ValidateTransactionBehaviour.auto_behaviour_id() - ) - assert ( - cast( - ValidateTransactionBehaviour, self.behaviour.current_behaviour - ).params.mutable_params.tx_hash - == "" - ) - - def test_sender_act_tx_data_contains_tx_digest(self) -> None: - """Test finalize behaviour.""" - - max_priority_fee_per_gas: Optional[int] = None - - retries = 1 - participants = ( - self.skill.skill_context.agent_address, - "a_1" + "-" * 39, - "a_2" + "-" * 39, - ) - kwargs = dict( - safe_contract_address="safe_contract_address", - participants=participants, - participant_to_signature={}, - most_voted_tx_hash=hash_payload_to_hex( - "b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", - 1, - 1, - "0x77E9b2EF921253A171Fa0CB9ba80558648Ff7215", - b"b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", - ), - nonce=None, - max_priority_fee_per_gas=max_priority_fee_per_gas, - keepers=retries.to_bytes(32, "big").hex() - + self.skill.skill_context.agent_address, - ) - - db = AbciAppDB(setup_data=AbciAppDB.data_to_lists(kwargs)) - - self.fast_forward_to_behaviour( - behaviour=self.behaviour, - behaviour_id=self.behaviour_class.auto_behaviour_id(), - synchronized_data=TransactionSettlementSynchronizedSata(db), - ) - - response_kwargs = dict( - performative=ContractApiMessage.Performative.RAW_TRANSACTION, - callable="get_deploy_transaction", - raw_transaction=RawTransaction( - ledger_id="ethereum", - body={ - "tx_hash": "0x3b", - "nonce": 0, - "maxFeePerGas": int(10e10), - "maxPriorityFeePerGas": int(10e10), - }, - ), - ) - - # mock the returned tx_data - return_value = dict( - status=VerificationStatus.PENDING, - keepers=deque(), - keeper_retries=1, - blacklisted_keepers=set(), - tx_digest="dummy_tx_digest", - ) - - current_behaviour = cast( - TransactionSettlementBaseBehaviour, self.behaviour.current_behaviour - ) - current_behaviour._get_tx_data = mock_yield_and_return(return_value) # type: ignore - - self.behaviour.act_wrapper() - self.mock_contract_api_request( - request_kwargs=dict( - performative=ContractApiMessage.Performative.GET_RAW_TRANSACTION, - ), - contract_id=str(GNOSIS_SAFE_CONTRACT_ID), - response_kwargs=response_kwargs, - ) - self.behaviour.act_wrapper() - self.mock_a2a_transaction() - self._test_done_flag_set() - self.end_round(TransactionSettlementEvent.DONE) - - current_behaviour = cast( - ValidateTransactionBehaviour, self.behaviour.current_behaviour - ) - current_behaviour_id = current_behaviour.behaviour_id - expected_behaviour_id = ValidateTransactionBehaviour.auto_behaviour_id() - assert current_behaviour_id == expected_behaviour_id - - def test_handle_late_messages(self) -> None: - """Test `handle_late_messages.`""" - participants = (self.skill.skill_context.agent_address, "a_1", "a_2") - self.fast_forward_to_behaviour( - behaviour=self.behaviour, - behaviour_id=self.behaviour_class.auto_behaviour_id(), - synchronized_data=TransactionSettlementSynchronizedSata( - AbciAppDB( - setup_data=AbciAppDB.data_to_lists( - dict( - most_voted_keeper_address="most_voted_keeper_address", - participants=participants, - keepers="keepers", - ) - ), - ) - ), - ) - self.behaviour.current_behaviour = cast( - BaseBehaviour, self.behaviour.current_behaviour - ) - assert ( - self.behaviour.current_behaviour.behaviour_id - == self.behaviour_class.auto_behaviour_id() - ) - - message = ContractApiMessage(ContractApiMessage.Performative.RAW_MESSAGE) # type: ignore - self.behaviour.current_behaviour.handle_late_messages( - self.behaviour.current_behaviour.behaviour_id, message - ) - assert cast( - TransactionSettlementBaseBehaviour, self.behaviour.current_behaviour - ).params.mutable_params.late_messages == [message] - - with mock.patch.object(self.behaviour.context.logger, "warning") as mock_info: - self.behaviour.current_behaviour.handle_late_messages( - "other_behaviour_id", message - ) - mock_info.assert_called_with( - f"No callback defined for request with nonce: {message.dialogue_reference[0]}, " - "arriving for behaviour: other_behaviour_id" - ) - message = MagicMock() - self.behaviour.current_behaviour.handle_late_messages( - self.behaviour.current_behaviour.behaviour_id, message - ) - mock_info.assert_called_with( - f"No callback defined for request with nonce: {message.dialogue_reference[0]}, " - f"arriving for behaviour: {FinalizeBehaviour.auto_behaviour_id()}" - ) - - -class TestValidateTransactionBehaviour(TransactionSettlementFSMBehaviourBaseCase): - """Test ValidateTransactionBehaviour.""" - - def _fast_forward(self) -> None: - """Fast-forward to relevant behaviour.""" - participants = (self.skill.skill_context.agent_address, "a_1", "a_2") - most_voted_keeper_address = self.skill.skill_context.agent_address - self.fast_forward_to_behaviour( - behaviour=self.behaviour, - behaviour_id=ValidateTransactionBehaviour.auto_behaviour_id(), - synchronized_data=TransactionSettlementSynchronizedSata( - AbciAppDB( - setup_data=AbciAppDB.data_to_lists( - dict( - safe_contract_address="safe_contract_address", - tx_hashes_history="t" * 66, - final_tx_hash="dummy_hash", - participants=participants, - most_voted_keeper_address=most_voted_keeper_address, - participant_to_signature={}, - most_voted_tx_hash=hash_payload_to_hex( - "b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", - 1, - 1, - "0x77E9b2EF921253A171Fa0CB9ba80558648Ff7215", - b"b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", - ), - max_priority_fee_per_gas=int(10e10), - ) - ), - ) - ), - ) - assert ( - cast( - BaseBehaviour, - cast(BaseBehaviour, self.behaviour.current_behaviour), - ).behaviour_id - == ValidateTransactionBehaviour.auto_behaviour_id() - ) - - def test_validate_transaction_safe_behaviour( - self, - ) -> None: - """Test ValidateTransactionBehaviour.""" - self._fast_forward() - self.behaviour.act_wrapper() - self.mock_ledger_api_request( - request_kwargs=dict( - performative=LedgerApiMessage.Performative.GET_TRANSACTION_RECEIPT - ), - response_kwargs=dict( - performative=LedgerApiMessage.Performative.TRANSACTION_RECEIPT, - transaction_receipt=TransactionReceipt( - ledger_id="ethereum", receipt={"status": 1}, transaction={} - ), - ), - ) - self.mock_contract_api_request( - request_kwargs=dict(performative=ContractApiMessage.Performative.GET_STATE), - contract_id=str(GNOSIS_SAFE_CONTRACT_ID), - response_kwargs=dict( - performative=ContractApiMessage.Performative.STATE, - callable="get_deploy_transaction", - state=TrState(ledger_id="ethereum", body={"verified": True}), - ), - ) - self.mock_a2a_transaction() - self._test_done_flag_set() - self.end_round(TransactionSettlementEvent.DONE) - behaviour = cast(BaseBehaviour, self.behaviour.current_behaviour) - assert ( - behaviour.behaviour_id - == make_degenerate_behaviour( - FinishedTransactionSubmissionRound - ).auto_behaviour_id() - ) - - def test_validate_transaction_safe_behaviour_no_tx_sent( - self, - ) -> None: - """Test ValidateTransactionBehaviour when tx cannot be sent.""" - self._fast_forward() - - with mock.patch.object(self.behaviour.context.logger, "error") as mock_logger: - self.behaviour.act_wrapper() - self.mock_ledger_api_request( - request_kwargs=dict( - performative=LedgerApiMessage.Performative.GET_TRANSACTION_RECEIPT, - ), - response_kwargs=dict( - performative=LedgerApiMessage.Performative.ERROR, - code=1, - ), - ) - behaviour = cast( - TransactionSettlementBaseBehaviour, - self.behaviour.current_behaviour, - ) - latest_tx_hash = behaviour.synchronized_data.tx_hashes_history[-1] - mock_logger.assert_any_call(f"tx {latest_tx_hash} receipt check timed out!") - - -class TestCheckTransactionHistoryBehaviour(TransactionSettlementFSMBehaviourBaseCase): - """Test CheckTransactionHistoryBehaviour.""" - - def _fast_forward(self, hashes_history: str) -> None: - """Fast-forward to relevant behaviour.""" - self.fast_forward_to_behaviour( - behaviour=self.behaviour, - behaviour_id=CheckTransactionHistoryBehaviour.auto_behaviour_id(), - synchronized_data=TransactionSettlementSynchronizedSata( - AbciAppDB( - setup_data=AbciAppDB.data_to_lists( - dict( - safe_contract_address="safe_contract_address", - participants=( - self.skill.skill_context.agent_address, - "a_1", - "a_2", - ), - participant_to_signature={}, - most_voted_tx_hash="b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002625a000x77E9b2EF921253A171Fa0CB9ba80558648Ff7215b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", - tx_hashes_history=hashes_history, - ) - ), - ) - ), - ) - assert ( - cast(BaseBehaviour, self.behaviour.current_behaviour).behaviour_id - == CheckTransactionHistoryBehaviour.auto_behaviour_id() - ) - - @pytest.mark.parametrize( - "verified, status, hashes_history, revert_reason", - ( - (False, -1, "0x" + "t" * 64, "test"), - (False, 0, "", "test"), - (False, 0, "0x" + "t" * 64, "test"), - (False, 0, "0x" + "t" * 64, "GS026"), - (True, 1, "0x" + "t" * 64, "test"), - ), - ) - def test_check_tx_history_behaviour( - self, - verified: bool, - status: int, - hashes_history: str, - revert_reason: str, - ) -> None: - """Test CheckTransactionHistoryBehaviour.""" - self._fast_forward(hashes_history) - self.behaviour.act_wrapper() - - if hashes_history: - self.mock_contract_api_request( - request_kwargs=dict( - performative=ContractApiMessage.Performative.GET_STATE - ), - contract_id=str(GNOSIS_SAFE_CONTRACT_ID), - response_kwargs=dict( - performative=ContractApiMessage.Performative.STATE, - callable="get_safe_nonce", - state=TrState( - ledger_id="ethereum", - body={ - "safe_nonce": 0, - }, - ), - ), - ) - self.mock_contract_api_request( - request_kwargs=dict( - performative=ContractApiMessage.Performative.GET_STATE - ), - contract_id=str(GNOSIS_SAFE_CONTRACT_ID), - response_kwargs=dict( - performative=ContractApiMessage.Performative.STATE, - callable="verify_tx", - state=TrState( - ledger_id="ethereum", - body={ - "verified": verified, - "status": status, - "transaction": {}, - }, - ), - ), - ) - - if not verified and status != -1: - self.mock_contract_api_request( - request_kwargs=dict( - performative=ContractApiMessage.Performative.GET_STATE - ), - contract_id=str(GNOSIS_SAFE_CONTRACT_ID), - response_kwargs=dict( - performative=ContractApiMessage.Performative.STATE, - callable="revert_reason", - state=TrState( - ledger_id="ethereum", body={"revert_reason": revert_reason} - ), - ), - ) - self.behaviour.act_wrapper() - self.mock_a2a_transaction() - self._test_done_flag_set() - self.end_round(TransactionSettlementEvent.DONE) - behaviour = cast(BaseBehaviour, self.behaviour.current_behaviour) - assert ( - behaviour.behaviour_id - == make_degenerate_behaviour( - FinishedTransactionSubmissionRound - ).auto_behaviour_id() - ) - - @pytest.mark.parametrize( - "verified, status, hashes_history, revert_reason", - ((False, 0, "0x" + "t" * 64, "test"),), - ) - def test_check_tx_history_behaviour_negative( - self, - verified: bool, - status: int, - hashes_history: str, - revert_reason: str, - ) -> None: - """Test CheckTransactionHistoryBehaviour.""" - self._fast_forward(hashes_history) - self.behaviour.act_wrapper() - self.behaviour.context.params.mutable_params.nonce = 1 - if hashes_history: - self.mock_contract_api_request( - request_kwargs=dict( - performative=ContractApiMessage.Performative.GET_STATE - ), - contract_id=str(GNOSIS_SAFE_CONTRACT_ID), - response_kwargs=dict( - performative=ContractApiMessage.Performative.STATE, - callable="get_safe_nonce", - state=TrState( - ledger_id="ethereum", - body={ - "safe_nonce": 1, - }, - ), - ), - ) - self.behaviour.act_wrapper() - self.mock_a2a_transaction() - self._test_done_flag_set() - self.end_round(TransactionSettlementEvent.DONE) - behaviour = cast(BaseBehaviour, self.behaviour.current_behaviour) - assert ( - behaviour.behaviour_id - == make_degenerate_behaviour( - FinishedTransactionSubmissionRound - ).auto_behaviour_id() - ) - - -class TestCheckLateTxHashesBehaviour(TransactionSettlementFSMBehaviourBaseCase): - """Test CheckLateTxHashesBehaviour.""" - - def _fast_forward(self, late_arriving_tx_hashes: Dict[str, str]) -> None: - """Fast-forward to relevant behaviour.""" - - agent_address = self.skill.skill_context.agent_address - kwargs = dict( - safe_contract_address="safe_contract_address", - participants=(agent_address, "a_1", "a_2"), - participant_to_signature={}, - most_voted_tx_hash="", - late_arriving_tx_hashes=late_arriving_tx_hashes, - ) - abci_app_db = AbciAppDB(setup_data=AbciAppDB.data_to_lists(kwargs)) - - self.fast_forward_to_behaviour( - behaviour=self.behaviour, - behaviour_id=CheckLateTxHashesBehaviour.auto_behaviour_id(), - synchronized_data=TransactionSettlementSynchronizedSata(abci_app_db), - ) - - current_behaviour = self.behaviour.current_behaviour - current_behaviour_id = cast(BaseBehaviour, current_behaviour).behaviour_id - assert current_behaviour_id == CheckLateTxHashesBehaviour.auto_behaviour_id() - - def test_check_tx_history_behaviour(self) -> None: - """Test CheckTransactionHistoryBehaviour.""" - self._fast_forward(late_arriving_tx_hashes={}) - self.behaviour.act_wrapper() - self.behaviour.act_wrapper() - self.mock_a2a_transaction() - self._test_done_flag_set() - self.end_round(TransactionSettlementEvent.DONE) - behaviour = cast(BaseBehaviour, self.behaviour.current_behaviour) - next_degen_behaviour = make_degenerate_behaviour( - FinishedTransactionSubmissionRound - ) - assert behaviour.behaviour_id == next_degen_behaviour.auto_behaviour_id() - - -class TestSynchronizeLateMessagesBehaviour(TransactionSettlementFSMBehaviourBaseCase): - """Test `SynchronizeLateMessagesBehaviour`""" - - def _check_behaviour_id( - self, expected: Type[TransactionSettlementBaseBehaviour] - ) -> None: - behaviour = cast(BaseBehaviour, self.behaviour.current_behaviour) - assert behaviour.behaviour_id == expected.auto_behaviour_id() - - @pytest.mark.parametrize("late_messages", ([], [MagicMock, MagicMock])) - def test_async_act(self, late_messages: List[MagicMock]) -> None: - """Test `async_act`""" - cast( - TransactionSettlementBaseBehaviour, self.behaviour.current_behaviour - ).params.mutable_params.late_messages = late_messages - - participants = (self.skill.skill_context.agent_address, "a_1", "a_2") - self.fast_forward_to_behaviour( - behaviour=self.behaviour, - behaviour_id=SynchronizeLateMessagesBehaviour.auto_behaviour_id(), - synchronized_data=TransactionSettlementSynchronizedSata( - AbciAppDB( - setup_data=dict( - participants=[participants], - participant_to_signature=[{}], - safe_contract_address=["safe_contract_address"], - most_voted_tx_hash=[ - hash_payload_to_hex( - "b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", - 1, - 1, - "0x77E9b2EF921253A171Fa0CB9ba80558648Ff7215", - b"b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9" - b"b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", - ) - ], - ), - ) - ), - ) - self._check_behaviour_id(SynchronizeLateMessagesBehaviour) # type: ignore - - if not late_messages: - self.behaviour.act_wrapper() - self.mock_a2a_transaction() - self._test_done_flag_set() - self.end_round(TransactionSettlementEvent.DONE) - self._check_behaviour_id(CheckLateTxHashesBehaviour) # type: ignore - - else: - - def _dummy_get_tx_data( - _current_message: ContractApiMessage, - _use_flashbots: bool, - chain_id: Optional[str] = None, - ) -> Generator[None, None, TxDataType]: - yield - return { - "status": VerificationStatus.PENDING, - "tx_digest": "test", - } - - cast( - TransactionSettlementBaseBehaviour, self.behaviour.current_behaviour - )._get_tx_data = _dummy_get_tx_data # type: ignore - for _ in range(len(late_messages)): - self.behaviour.act_wrapper() - - -class TestResetBehaviour(TransactionSettlementFSMBehaviourBaseCase): - """Test the reset behaviour.""" - - behaviour_class = ResetBehaviour - next_behaviour_class = RandomnessTransactionSubmissionBehaviour - - def test_reset_behaviour( - self, - ) -> None: - """Test reset behaviour.""" - self.fast_forward_to_behaviour( - behaviour=self.behaviour, - behaviour_id=self.behaviour_class.auto_behaviour_id(), - synchronized_data=TransactionSettlementSynchronizedSata( - AbciAppDB(setup_data=dict(estimate=[1.0])), - ), - ) - assert ( - cast( - BaseBehaviour, - cast(BaseBehaviour, self.behaviour.current_behaviour), - ).behaviour_id - == self.behaviour_class.auto_behaviour_id() - ) - self.behaviour.context.params.__dict__["reset_pause_duration"] = 0.1 - self.behaviour.act_wrapper() - time.sleep(0.3) - self.behaviour.act_wrapper() - self.mock_a2a_transaction() - self._test_done_flag_set() - self.end_round(TransactionSettlementEvent.DONE) - behaviour = cast(BaseBehaviour, self.behaviour.current_behaviour) - assert behaviour.behaviour_id == self.next_behaviour_class.auto_behaviour_id() diff --git a/packages/valory/skills/transaction_settlement_abci/tests/test_dialogues.py b/packages/valory/skills/transaction_settlement_abci/tests/test_dialogues.py deleted file mode 100644 index 519ea4d..0000000 --- a/packages/valory/skills/transaction_settlement_abci/tests/test_dialogues.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test the dialogues.py module of the skill.""" - -# pylint: skip-file - -import packages.valory.skills.transaction_settlement_abci.dialogues # noqa - - -def test_import() -> None: - """Test that the 'dialogues.py' Python module can be imported.""" diff --git a/packages/valory/skills/transaction_settlement_abci/tests/test_handlers.py b/packages/valory/skills/transaction_settlement_abci/tests/test_handlers.py deleted file mode 100644 index c458846..0000000 --- a/packages/valory/skills/transaction_settlement_abci/tests/test_handlers.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2022 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test the dialogues.py module of the skill.""" - -# pylint: skip-file - -import packages.valory.skills.transaction_settlement_abci.handlers # noqa - - -def test_import() -> None: - """Test that the 'handlers.py' Python module can be imported.""" diff --git a/packages/valory/skills/transaction_settlement_abci/tests/test_models.py b/packages/valory/skills/transaction_settlement_abci/tests/test_models.py deleted file mode 100644 index 746f0a3..0000000 --- a/packages/valory/skills/transaction_settlement_abci/tests/test_models.py +++ /dev/null @@ -1,95 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2022-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ -# pylint: disable=unused-import - -"""Test the models.py module of the skill.""" -from typing import Any, Dict -from unittest.mock import MagicMock - -import pytest -import yaml -from aea.exceptions import AEAEnforceError - -from packages.valory.skills.abstract_round_abci.test_tools.base import DummyContext -from packages.valory.skills.transaction_settlement_abci.models import ( - TransactionParams, - _MINIMUM_VALIDATE_TIMEOUT, -) -from packages.valory.skills.transaction_settlement_abci.tests import PACKAGE_DIR - - -class TestTransactionParams: # pylint: disable=too-few-public-methods - """Test TransactionParams class.""" - - default_config: Dict - - def setup_class(self) -> None: - """Read the default config only once.""" - skill_yaml = PACKAGE_DIR / "skill.yaml" - with open(skill_yaml, "r", encoding="utf-8") as skill_file: - skill = yaml.safe_load(skill_file) - self.default_config = skill["models"]["params"]["args"] - - def test_ensure_validate_timeout( # pylint: disable=no-self-use - self, - ) -> None: - """Test that `_ensure_validate_timeout` raises when `validate_timeout` is lower than the allowed minimum.""" - dummy_value = 0 - mock_args, mock_kwargs = ( - MagicMock(), - { - **self.default_config, - "validate_timeout": dummy_value, - "skill_context": DummyContext(), - }, - ) - with pytest.raises( - expected_exception=AEAEnforceError, - match=f"`validate_timeout` must be greater than or equal to {_MINIMUM_VALIDATE_TIMEOUT}", - ): - TransactionParams(mock_args, **mock_kwargs) - - @pytest.mark.parametrize( - "gas_params", - [ - {}, - {"gas_price": 1}, - {"max_fee_per_gas": 1}, - {"max_priority_fee_per_gas": 1}, - { - "gas_price": 1, - "max_fee_per_gas": 1, - "max_priority_fee_per_gas": 1, - }, - ], - ) - def test_gas_params(self, gas_params: Dict[str, Any]) -> None: - """Test that gas params are being handled properly.""" - mock_args, mock_kwargs = ( - MagicMock(), - { - **self.default_config, - "gas_params": gas_params, - "skill_context": DummyContext(), - }, - ) - params = TransactionParams(mock_args, **mock_kwargs) - # verify that the gas params are being set properly - for key, value in gas_params.items(): - assert getattr(params.gas_params, key) == value diff --git a/packages/valory/skills/transaction_settlement_abci/tests/test_payload_tools.py b/packages/valory/skills/transaction_settlement_abci/tests/test_payload_tools.py deleted file mode 100644 index 91a90cf..0000000 --- a/packages/valory/skills/transaction_settlement_abci/tests/test_payload_tools.py +++ /dev/null @@ -1,101 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Tests for valory/transaction settlement skill's payload tools.""" - -# pylint: skip-file - -import pytest - -from packages.valory.contracts.gnosis_safe.contract import SafeOperation -from packages.valory.skills.transaction_settlement_abci.payload_tools import ( - NULL_ADDRESS, - PayloadDeserializationError, - VerificationStatus, - hash_payload_to_hex, - skill_input_hex_to_payload, - tx_hist_hex_to_payload, - tx_hist_payload_to_hex, -) - - -class TestTxHistPayloadEncodingDecoding: - """Tests for the transaction history's payload encoding - decoding.""" - - @staticmethod - @pytest.mark.parametrize( - "verification_status, tx_hash", - ( - ( - VerificationStatus.VERIFIED, - "0xb0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", - ), - (VerificationStatus.ERROR, None), - ), - ) - def test_tx_hist_payload_to_hex_and_back( - verification_status: VerificationStatus, tx_hash: str - ) -> None: - """Test `tx_hist_payload_to_hex` and `tx_hist_hex_to_payload` functions.""" - intermediate = tx_hist_payload_to_hex(verification_status, tx_hash) - verification_status_, tx_hash_ = tx_hist_hex_to_payload(intermediate) - assert verification_status == verification_status_ - assert tx_hash == tx_hash_ - - @staticmethod - def test_invalid_tx_hash_during_serialization() -> None: - """Test encoding when transaction hash is invalid.""" - with pytest.raises(ValueError): - tx_hist_payload_to_hex(VerificationStatus.VERIFIED, "") - - @staticmethod - @pytest.mark.parametrize( - "payload", - ("0000000000000000000000000000000000000000000000000000000000000008", ""), - ) - def test_invalid_payloads_during_deserialization(payload: str) -> None: - """Test decoding payload is invalid.""" - with pytest.raises(PayloadDeserializationError): - tx_hist_hex_to_payload(payload) - - -@pytest.mark.parametrize("use_flashbots", (True, False)) -def test_payload_to_hex_and_back(use_flashbots: bool) -> None: - """Test `payload_to_hex` function.""" - tx_params = dict( - safe_tx_hash="b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", - ether_value=0, - safe_tx_gas=40000000, - to_address="0x77E9b2EF921253A171Fa0CB9ba80558648Ff7215", - data=( - b"b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9" - b"b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9" - ), - operation=SafeOperation.CALL.value, - base_gas=0, - safe_gas_price=0, - gas_token=NULL_ADDRESS, - use_flashbots=use_flashbots, - gas_limit=0, - refund_receiver=NULL_ADDRESS, - raise_on_failed_simulation=False, - ) - - intermediate = hash_payload_to_hex(**tx_params) # type: ignore - assert tx_params == skill_input_hex_to_payload(intermediate) diff --git a/packages/valory/skills/transaction_settlement_abci/tests/test_payloads.py b/packages/valory/skills/transaction_settlement_abci/tests/test_payloads.py deleted file mode 100644 index e53cd26..0000000 --- a/packages/valory/skills/transaction_settlement_abci/tests/test_payloads.py +++ /dev/null @@ -1,126 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test the payloads of the skill.""" - -# pylint: skip-file - -from typing import Optional - -import pytest - -from packages.valory.skills.transaction_settlement_abci.payloads import ( - CheckTransactionHistoryPayload, - FinalizationTxPayload, - RandomnessPayload, - ResetPayload, - SelectKeeperPayload, - SignaturePayload, - SynchronizeLateMessagesPayload, - ValidatePayload, -) - - -def test_randomness_payload() -> None: - """Test `RandomnessPayload`.""" - - payload = RandomnessPayload(sender="sender", round_id=1, randomness="test") - - assert payload.round_id == 1 - assert payload.randomness == "test" - assert payload.data == {"round_id": 1, "randomness": "test"} - - -def test_select_keeper_payload() -> None: - """Test `SelectKeeperPayload`.""" - - payload = SelectKeeperPayload(sender="sender", keepers="test") - - assert payload.keepers == "test" - assert payload.data == {"keepers": "test"} - - -@pytest.mark.parametrize("vote", (None, True, False)) -def test_validate_payload(vote: Optional[bool]) -> None: - """Test `ValidatePayload`.""" - - payload = ValidatePayload(sender="sender", vote=vote) - - assert payload.vote is vote - assert payload.data == {"vote": vote} - - -def test_tx_history_payload() -> None: - """Test `CheckTransactionHistoryPayload`.""" - - payload = CheckTransactionHistoryPayload(sender="sender", verified_res="test") - - assert payload.verified_res == "test" - assert payload.data == {"verified_res": "test"} - - -def test_synchronize_payload() -> None: - """Test `SynchronizeLateMessagesPayload`.""" - - tx_hashes = "test" - payload = SynchronizeLateMessagesPayload(sender="sender", tx_hashes=tx_hashes) - - assert payload.tx_hashes == tx_hashes - assert payload.data == {"tx_hashes": tx_hashes} - - -def test_signature_payload() -> None: - """Test `SignaturePayload`.""" - - payload = SignaturePayload(sender="sender", signature="sign") - - assert payload.signature == "sign" - assert payload.data == {"signature": "sign"} - - -def test_finalization_tx_payload() -> None: - """Test `FinalizationTxPayload`.""" - - payload = FinalizationTxPayload( - sender="sender", - tx_data={ - "tx_digest": "hash", - "nonce": 0, - "max_fee_per_gas": 0, - "max_priority_fee_per_gas": 0, - }, - ) - - assert payload.data == { - "tx_data": { - "tx_digest": "hash", - "nonce": 0, - "max_fee_per_gas": 0, - "max_priority_fee_per_gas": 0, - } - } - - -def test_reset_payload() -> None: - """Test `ResetPayload`.""" - - payload = ResetPayload(sender="sender", period_count=1) - - assert payload.period_count == 1 - assert payload.data == {"period_count": 1} diff --git a/packages/valory/skills/transaction_settlement_abci/tests/test_rounds.py b/packages/valory/skills/transaction_settlement_abci/tests/test_rounds.py deleted file mode 100644 index ecdb1af..0000000 --- a/packages/valory/skills/transaction_settlement_abci/tests/test_rounds.py +++ /dev/null @@ -1,1023 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2021-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Tests for valory/registration_abci skill's rounds.""" - -# pylint: skip-file - -import hashlib -import logging # noqa: F401 -from collections import deque -from typing import ( - Any, - Deque, - Dict, - FrozenSet, - List, - Mapping, - Optional, - Type, - Union, - cast, -) -from unittest import mock -from unittest.mock import MagicMock - -import pytest - -from packages.valory.skills.abstract_round_abci.base import ( - ABCIAppInternalError, - AbciAppDB, -) -from packages.valory.skills.abstract_round_abci.base import ( - BaseSynchronizedData as SynchronizedData, -) -from packages.valory.skills.abstract_round_abci.base import ( - BaseTxPayload, - CollectSameUntilThresholdRound, - CollectionRound, - MAX_INT_256, - TransactionNotValidError, - VotingRound, -) -from packages.valory.skills.abstract_round_abci.test_tools.rounds import ( - BaseCollectDifferentUntilThresholdRoundTest, - BaseCollectNonEmptyUntilThresholdRound, - BaseCollectSameUntilThresholdRoundTest, - BaseOnlyKeeperSendsRoundTest, - BaseVotingRoundTest, -) -from packages.valory.skills.transaction_settlement_abci.payload_tools import ( - VerificationStatus, -) -from packages.valory.skills.transaction_settlement_abci.payloads import ( - CheckTransactionHistoryPayload, - FinalizationTxPayload, - RandomnessPayload, - ResetPayload, - SelectKeeperPayload, - SignaturePayload, - SynchronizeLateMessagesPayload, - ValidatePayload, -) -from packages.valory.skills.transaction_settlement_abci.rounds import ( - CheckTransactionHistoryRound, - CollectSignatureRound, -) -from packages.valory.skills.transaction_settlement_abci.rounds import ( - Event as TransactionSettlementEvent, -) -from packages.valory.skills.transaction_settlement_abci.rounds import ( - FinalizationRound, - ResetRound, - SelectKeeperTransactionSubmissionARound, - SelectKeeperTransactionSubmissionBAfterTimeoutRound, - SelectKeeperTransactionSubmissionBRound, - SynchronizeLateMessagesRound, -) -from packages.valory.skills.transaction_settlement_abci.rounds import ( - SynchronizedData as TransactionSettlementSynchronizedSata, -) -from packages.valory.skills.transaction_settlement_abci.rounds import ( - TX_HASH_LENGTH, - ValidateTransactionRound, -) - - -MAX_PARTICIPANTS: int = 4 -RANDOMNESS: str = "d1c29dce46f979f9748210d24bce4eae8be91272f5ca1a6aea2832d3dd676f51" -DUMMY_RANDOMNESS = hashlib.sha256("hash".encode() + str(0).encode()).hexdigest() - - -def get_participants() -> FrozenSet[str]: - """Participants""" - return frozenset([f"agent_{i}" for i in range(MAX_PARTICIPANTS)]) - - -def get_participant_to_randomness( - participants: FrozenSet[str], round_id: int -) -> Dict[str, RandomnessPayload]: - """participant_to_randomness""" - return { - participant: RandomnessPayload( - sender=participant, - round_id=round_id, - randomness=RANDOMNESS, - ) - for participant in participants - } - - -def get_most_voted_randomness() -> str: - """most_voted_randomness""" - return RANDOMNESS - - -def get_participant_to_selection( - participants: FrozenSet[str], - keepers: str, -) -> Dict[str, SelectKeeperPayload]: - """participant_to_selection""" - return { - participant: SelectKeeperPayload(sender=participant, keepers=keepers) - for participant in participants - } - - -def get_participant_to_period_count( - participants: FrozenSet[str], period_count: int -) -> Dict[str, ResetPayload]: - """participant_to_selection""" - return { - participant: ResetPayload(sender=participant, period_count=period_count) - for participant in participants - } - - -def get_safe_contract_address() -> str: - """safe_contract_address""" - return "0x6f6ab56aca12" - - -def get_participant_to_votes( - participants: FrozenSet[str], vote: Optional[bool] = True -) -> Dict[str, ValidatePayload]: - """participant_to_votes""" - return { - participant: ValidatePayload(sender=participant, vote=vote) - for participant in participants - } - - -def get_participant_to_votes_serialized( - participants: FrozenSet[str], vote: Optional[bool] = True -) -> Dict[str, Dict[str, Any]]: - """participant_to_votes""" - return CollectionRound.serialize_collection( - get_participant_to_votes(participants, vote) - ) - - -def get_most_voted_tx_hash() -> str: - """most_voted_tx_hash""" - return "tx_hash" - - -def get_participant_to_signature( - participants: FrozenSet[str], -) -> Dict[str, SignaturePayload]: - """participant_to_signature""" - return { - participant: SignaturePayload(sender=participant, signature="signature") - for participant in participants - } - - -def get_final_tx_hash() -> str: - """final_tx_hash""" - return "tx_hash" - - -def get_participant_to_check( - participants: FrozenSet[str], - status: str, - tx_hash: str, -) -> Dict[str, CheckTransactionHistoryPayload]: - """Get participants to check""" - return { - participant: CheckTransactionHistoryPayload( - sender=participant, - verified_res=status + tx_hash, - ) - for participant in participants - } - - -def get_participant_to_late_arriving_tx_hashes( - participants: FrozenSet[str], -) -> Dict[str, SynchronizeLateMessagesPayload]: - """participant_to_selection""" - return { - participant: SynchronizeLateMessagesPayload( - sender=participant, tx_hashes="1" * TX_HASH_LENGTH + "2" * TX_HASH_LENGTH - ) - for participant in participants - } - - -def get_late_arriving_tx_hashes_deserialized() -> Dict[str, List[str]]: - """Get dummy late-arriving tx hashes.""" - # We want the tx hashes to have a size which can be divided by 64 to be able to parse it. - # Otherwise, they are not valid. - return { - "sender": [ - "t" * TX_HASH_LENGTH, - "e" * TX_HASH_LENGTH, - "s" * TX_HASH_LENGTH, - "t" * TX_HASH_LENGTH, - ] - } - - -def get_late_arriving_tx_hashes_serialized() -> Dict[str, str]: - """Get dummy late-arriving tx hashes.""" - # We want the tx hashes to have a size which can be divided by 64 to be able to parse it. - # Otherwise, they are not valid. - deserialized = get_late_arriving_tx_hashes_deserialized() - return {sender: "".join(hash_) for sender, hash_ in deserialized.items()} - - -def get_keepers(keepers: Deque[str], retries: int = 1) -> str: - """Get dummy keepers.""" - return retries.to_bytes(32, "big").hex() + "".join(keepers) - - -class BaseValidateRoundTest(BaseVotingRoundTest): - """Test BaseValidateRound.""" - - test_class: Type[VotingRound] - test_payload: Type[ValidatePayload] - - def test_positive_votes( - self, - ) -> None: - """Test ValidateRound.""" - - self.synchronized_data.update(tx_hashes_history="t" * 66) - - test_round = self.test_class( - synchronized_data=self.synchronized_data, - context=MagicMock(), - ) - - self._complete_run( - self._test_voting_round_positive( - test_round=test_round, - round_payloads=get_participant_to_votes(self.participants), - synchronized_data_update_fn=lambda _synchronized_data, _: _synchronized_data.update( - participant_to_votes=get_participant_to_votes_serialized( - self.participants - ) - ), - synchronized_data_attr_checks=[ - lambda _synchronized_data: _synchronized_data.participant_to_votes.keys() - ], - exit_event=self._event_class.DONE, - ) - ) - - def test_negative_votes( - self, - ) -> None: - """Test ValidateRound.""" - - test_round = self.test_class( - synchronized_data=self.synchronized_data, - context=MagicMock(), - ) - - self._complete_run( - self._test_voting_round_negative( - test_round=test_round, - round_payloads=get_participant_to_votes(self.participants, vote=False), - synchronized_data_update_fn=lambda _synchronized_data, _: _synchronized_data.update( - participant_to_votes=get_participant_to_votes_serialized( - self.participants, vote=False - ) - ), - synchronized_data_attr_checks=[], - exit_event=self._event_class.NEGATIVE, - ) - ) - - def test_none_votes( - self, - ) -> None: - """Test ValidateRound.""" - - test_round = self.test_class( - synchronized_data=self.synchronized_data, - context=MagicMock(), - ) - - self._complete_run( - self._test_voting_round_none( - test_round=test_round, - round_payloads=get_participant_to_votes(self.participants, vote=None), - synchronized_data_update_fn=lambda _synchronized_data, _: _synchronized_data.update( - participant_to_votes=get_participant_to_votes_serialized( - self.participants, vote=None - ) - ), - synchronized_data_attr_checks=[], - exit_event=self._event_class.NONE, - ) - ) - - -class BaseSelectKeeperRoundTest(BaseCollectSameUntilThresholdRoundTest): - """Test SelectKeeperTransactionSubmissionARound""" - - test_class: Type[CollectSameUntilThresholdRound] - test_payload: Type[BaseTxPayload] - - _synchronized_data_class = SynchronizedData - - @staticmethod - def _participant_to_selection( - participants: FrozenSet[str], keepers: str - ) -> Mapping[str, BaseTxPayload]: - """Get participant to selection""" - return get_participant_to_selection(participants, keepers) - - def test_run( - self, - most_voted_payload: str = "keeper", - keepers: str = "", - exit_event: Optional[Any] = None, - ) -> None: - """Run tests.""" - test_round = self.test_class( - synchronized_data=self.synchronized_data.update( - keepers=keepers, - ), - context=MagicMock(), - ) - - self._complete_run( - self._test_round( - test_round=test_round, - round_payloads=self._participant_to_selection( - self.participants, most_voted_payload - ), - synchronized_data_update_fn=lambda _synchronized_data, _test_round: _synchronized_data.update( - participant_to_selection=CollectionRound.serialize_collection( - self._participant_to_selection( - self.participants, most_voted_payload - ) - ) - ), - synchronized_data_attr_checks=[ - lambda _synchronized_data: _synchronized_data.participant_to_selection.keys() - if exit_event is None - else None - ], - most_voted_payload=most_voted_payload, - exit_event=self._event_class.DONE if exit_event is None else exit_event, - ) - ) - - -class TestSelectKeeperTransactionSubmissionARound(BaseSelectKeeperRoundTest): - """Test SelectKeeperTransactionSubmissionARound""" - - test_class = SelectKeeperTransactionSubmissionARound - test_payload = SelectKeeperPayload - _synchronized_data_class = TransactionSettlementSynchronizedSata - _event_class = TransactionSettlementEvent - - @pytest.mark.parametrize( - "most_voted_payload, keepers, exit_event", - ( - ( - "incorrectly_serialized", - "", - TransactionSettlementEvent.INCORRECT_SERIALIZATION, - ), - ( - int(1).to_bytes(32, "big").hex() + "new_keeper" + "-" * 32, - "", - TransactionSettlementEvent.DONE, - ), - ), - ) - def test_run( - self, - most_voted_payload: str, - keepers: str, - exit_event: TransactionSettlementEvent, - ) -> None: - """Run tests.""" - super().test_run(most_voted_payload, keepers, exit_event) - - -class TestSelectKeeperTransactionSubmissionBRound( - TestSelectKeeperTransactionSubmissionARound -): - """Test SelectKeeperTransactionSubmissionBRound.""" - - test_class = SelectKeeperTransactionSubmissionBRound - - @pytest.mark.parametrize( - "most_voted_payload, keepers, exit_event", - ( - ( - int(1).to_bytes(32, "big").hex() + "new_keeper" + "-" * 32, - "", - TransactionSettlementEvent.DONE, - ), - ( - int(1).to_bytes(32, "big").hex() + "new_keeper" + "-" * 32, - int(1).to_bytes(32, "big").hex() - + "".join( - [keeper + "-" * 30 for keeper in ("test_keeper1", "test_keeper2")] - ), - TransactionSettlementEvent.DONE, - ), - ), - ) - def test_run( - self, - most_voted_payload: str, - keepers: str, - exit_event: TransactionSettlementEvent, - ) -> None: - """Run tests.""" - super().test_run(most_voted_payload, keepers, exit_event) - - -class TestSelectKeeperTransactionSubmissionBAfterTimeoutRound( - TestSelectKeeperTransactionSubmissionBRound -): - """Test SelectKeeperTransactionSubmissionBAfterTimeoutRound.""" - - test_class = SelectKeeperTransactionSubmissionBAfterTimeoutRound - - @mock.patch.object( - TransactionSettlementSynchronizedSata, - "keepers_threshold_exceeded", - new_callable=mock.PropertyMock, - ) - @pytest.mark.parametrize( - "keepers", (f"{int(1).to_bytes(32, 'big').hex()}keeper" + "-" * 36,) - ) - @pytest.mark.parametrize( - "attrs, threshold_exceeded, exit_event", - ( - ( - { - "tx_hashes_history": "t" * 66, - "missed_messages": {f"keeper{'-' * 36}": 10}, - }, - True, - # Since the threshold has been exceeded, we should return a `CHECK_HISTORY` event. - TransactionSettlementEvent.CHECK_HISTORY, - ), - ( - { - "missed_messages": {f"keeper{'-' * 36}": 10}, - }, - True, - TransactionSettlementEvent.CHECK_LATE_ARRIVING_MESSAGE, - ), - ( - { - "missed_messages": {f"keeper{'-' * 36}": 10}, - }, - False, - TransactionSettlementEvent.DONE, - ), - ), - ) - def test_run( - self, - threshold_exceeded_mock: mock.PropertyMock, - keepers: str, - attrs: Dict[str, Union[str, int]], - threshold_exceeded: bool, - exit_event: TransactionSettlementEvent, - ) -> None: - """Test `SelectKeeperTransactionSubmissionBAfterTimeoutRound`.""" - self.synchronized_data.update(participant_to_selection=dict.fromkeys(self.participants), **attrs) # type: ignore - threshold_exceeded_mock.return_value = threshold_exceeded - most_voted_payload = int(1).to_bytes(32, "big").hex() + "new_keeper" + "-" * 32 - super().test_run(most_voted_payload, keepers, exit_event) - initial_missed_messages = cast(Dict[str, int], (attrs["missed_messages"])) - expected_missed_messages = { - sender: missed + 1 for sender, missed in initial_missed_messages.items() - } - synchronized_data = cast( - TransactionSettlementSynchronizedSata, self.synchronized_data - ) - assert synchronized_data.missed_messages == expected_missed_messages - - -class TestFinalizationRound(BaseOnlyKeeperSendsRoundTest): - """Test FinalizationRound.""" - - _synchronized_data_class = TransactionSettlementSynchronizedSata - _event_class = TransactionSettlementEvent - _round_class = FinalizationRound - - @pytest.mark.parametrize( - "tx_hashes_history, tx_digest, missed_messages, status, exit_event", - ( - ( - "", - "", - {"test": 1}, - VerificationStatus.ERROR.value, - TransactionSettlementEvent.CHECK_LATE_ARRIVING_MESSAGE, - ), - ( - "", - "", - {}, - VerificationStatus.ERROR.value, - TransactionSettlementEvent.FINALIZATION_FAILED, - ), - ( - "t" * 66, - "", - {}, - VerificationStatus.VERIFIED.value, - TransactionSettlementEvent.CHECK_HISTORY, - ), - ( - "t" * 66, - "", - {}, - VerificationStatus.ERROR.value, - TransactionSettlementEvent.CHECK_HISTORY, - ), - ( - "", - "", - {}, - VerificationStatus.PENDING.value, - TransactionSettlementEvent.FINALIZATION_FAILED, - ), - ( - "", - "tx_digest" + "t" * 57, - {}, - VerificationStatus.PENDING.value, - TransactionSettlementEvent.DONE, - ), - ( - "t" * 66, - "tx_digest" + "t" * 57, - {}, - VerificationStatus.PENDING.value, - TransactionSettlementEvent.DONE, - ), - ( - "t" * 66, - "", - {}, - VerificationStatus.INSUFFICIENT_FUNDS.value, - TransactionSettlementEvent.INSUFFICIENT_FUNDS, - ), - ), - ) - def test_finalization_round( - self, - tx_hashes_history: str, - tx_digest: str, - missed_messages: int, - status: int, - exit_event: TransactionSettlementEvent, - ) -> None: - """Runs tests.""" - keeper_retries = 2 - blacklisted_keepers = "" - self.participants = frozenset([f"agent_{i}" + "-" * 35 for i in range(4)]) - keepers = deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35)) - self.synchronized_data = cast( - TransactionSettlementSynchronizedSata, - self.synchronized_data.update( - participants=tuple(self.participants), - missed_messages=missed_messages, - tx_hashes_history=tx_hashes_history, - keepers=get_keepers(keepers, keeper_retries), - blacklisted_keepers=blacklisted_keepers, - ), - ) - - sender = keepers[0] - tx_hashes_history += ( - tx_digest - if exit_event == TransactionSettlementEvent.DONE - else tx_hashes_history - ) - if status == VerificationStatus.INSUFFICIENT_FUNDS.value: - popped = keepers.popleft() - blacklisted_keepers += popped - keeper_retries = 1 - - test_round = self._round_class( - synchronized_data=self.synchronized_data, - context=MagicMock(), - ) - - self._complete_run( - self._test_round( - test_round=test_round, - keeper_payloads=FinalizationTxPayload( - sender=sender, - tx_data={ - "status_value": status, - "serialized_keepers": get_keepers(keepers, keeper_retries), - "blacklisted_keepers": blacklisted_keepers, - "tx_hashes_history": tx_hashes_history, - "received_hash": bool(tx_digest), - }, - ), - synchronized_data_update_fn=lambda _synchronized_data, _: _synchronized_data.update( - tx_hashes_history=tx_hashes_history, - blacklisted_keepers=blacklisted_keepers, - keepers=get_keepers(keepers, keeper_retries), - keeper_retries=keeper_retries, - final_verification_status=VerificationStatus(status).value, - ), - synchronized_data_attr_checks=[ - lambda _synchronized_data: _synchronized_data.tx_hashes_history, - lambda _synchronized_data: _synchronized_data.blacklisted_keepers, - lambda _synchronized_data: _synchronized_data.keepers, - lambda _synchronized_data: _synchronized_data.keeper_retries, - lambda _synchronized_data: _synchronized_data.final_verification_status, - ], - exit_event=exit_event, - ) - ) - - def test_finalization_round_no_tx_data(self) -> None: - """Test finalization round when `tx_data` is `None`.""" - keepers = deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35)) - keeper_retries = 2 - self.synchronized_data = cast( - TransactionSettlementSynchronizedSata, - self.synchronized_data.update( - participants=tuple(f"agent_{i}" + "-" * 35 for i in range(4)), - keepers=get_keepers(keepers, keeper_retries), - ), - ) - - sender = keepers[0] - - test_round = self._round_class( - synchronized_data=self.synchronized_data, - context=MagicMock(), - ) - - self._complete_run( - self._test_round( - test_round=test_round, - keeper_payloads=FinalizationTxPayload( - sender=sender, - tx_data=None, - ), - synchronized_data_update_fn=lambda _synchronized_data, _: _synchronized_data, - synchronized_data_attr_checks=[ - lambda _synchronized_data: _synchronized_data.tx_hashes_history, - lambda _synchronized_data: _synchronized_data.blacklisted_keepers, - lambda _synchronized_data: _synchronized_data.keepers, - lambda _synchronized_data: _synchronized_data.keeper_retries, - lambda _synchronized_data: _synchronized_data.final_verification_status, - ], - exit_event=TransactionSettlementEvent.FINALIZATION_FAILED, - ) - ) - - -class TestCollectSignatureRound(BaseCollectDifferentUntilThresholdRoundTest): - """Test CollectSignatureRound.""" - - _synchronized_data_class = TransactionSettlementSynchronizedSata - _event_class = TransactionSettlementEvent - - def test_run( - self, - ) -> None: - """Runs tests.""" - - test_round = CollectSignatureRound( - synchronized_data=self.synchronized_data, - context=MagicMock(), - ) - - self._complete_run( - self._test_round( - test_round=test_round, - round_payloads=get_participant_to_signature(self.participants), - synchronized_data_update_fn=lambda _synchronized_data, _: _synchronized_data, - synchronized_data_attr_checks=[], - exit_event=self._event_class.DONE, - ) - ) - - -class TestValidateTransactionRound(BaseValidateRoundTest): - """Test ValidateRound.""" - - test_class = ValidateTransactionRound - _event_class = TransactionSettlementEvent - _synchronized_data_class = TransactionSettlementSynchronizedSata - - -class TestCheckTransactionHistoryRound(BaseCollectSameUntilThresholdRoundTest): - """Test CheckTransactionHistoryRound""" - - _event_class = TransactionSettlementEvent - _synchronized_data_class = TransactionSettlementSynchronizedSata - - @pytest.mark.parametrize( - "expected_status, expected_tx_hash, missed_messages, expected_event", - ( - ( - "0000000000000000000000000000000000000000000000000000000000000001", - "b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", - {}, - TransactionSettlementEvent.DONE, - ), - ( - "0000000000000000000000000000000000000000000000000000000000000002", - "b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", - {}, - TransactionSettlementEvent.NEGATIVE, - ), - ( - "0000000000000000000000000000000000000000000000000000000000000003", - "b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", - {}, - TransactionSettlementEvent.NONE, - ), - ( - "0000000000000000000000000000000000000000000000000000000000000007", - "b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", - {}, - TransactionSettlementEvent.NONE, - ), - ( - "0000000000000000000000000000000000000000000000000000000000000002", - "b0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", - {"test": 1}, - TransactionSettlementEvent.CHECK_LATE_ARRIVING_MESSAGE, - ), - ), - ) - def test_run( - self, - expected_status: str, - expected_tx_hash: str, - missed_messages: int, - expected_event: TransactionSettlementEvent, - ) -> None: - """Run tests.""" - keepers = get_keepers(deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35))) - self.synchronized_data.update(missed_messages=missed_messages, keepers=keepers) - - test_round = CheckTransactionHistoryRound( - synchronized_data=self.synchronized_data, - context=MagicMock(), - ) - - self._complete_run( - self._test_round( - test_round=test_round, - round_payloads=get_participant_to_check( - self.participants, expected_status, expected_tx_hash - ), - synchronized_data_update_fn=lambda synchronized_data, _: synchronized_data.update( - participant_to_check=CollectionRound.serialize_collection( - get_participant_to_check( - self.participants, expected_status, expected_tx_hash - ) - ), - final_verification_status=int(expected_status), - tx_hashes_history=[expected_tx_hash], - keepers=keepers, - final_tx_hash="0xb0e6add595e00477cf347d09797b156719dc5233283ac76e4efce2a674fe72d9", - ), - synchronized_data_attr_checks=[ - lambda _synchronized_data: _synchronized_data.final_verification_status, - lambda _synchronized_data: _synchronized_data.final_tx_hash, - lambda _synchronized_data: _synchronized_data.keepers, - ] - if expected_event - not in { - TransactionSettlementEvent.NEGATIVE, - TransactionSettlementEvent.CHECK_LATE_ARRIVING_MESSAGE, - } - else [ - lambda _synchronized_data: _synchronized_data.final_verification_status, - lambda _synchronized_data: _synchronized_data.keepers, - ], - most_voted_payload=expected_status + expected_tx_hash, - exit_event=expected_event, - ) - ) - - -class TestSynchronizeLateMessagesRound(BaseCollectNonEmptyUntilThresholdRound): - """Test `SynchronizeLateMessagesRound`.""" - - _event_class = TransactionSettlementEvent - _synchronized_data_class = TransactionSettlementSynchronizedSata - - @pytest.mark.parametrize( - "missed_messages, expected_event", - ( - ( - {f"agent_{i}": 0 for i in range(4)}, - TransactionSettlementEvent.SUSPICIOUS_ACTIVITY, - ), - ({f"agent_{i}": 2 for i in range(4)}, TransactionSettlementEvent.DONE), - ), - ) - def test_runs( - self, missed_messages: int, expected_event: TransactionSettlementEvent - ) -> None: - """Runs tests.""" - self.synchronized_data.update(missed_messages=missed_messages) - test_round = SynchronizeLateMessagesRound( - synchronized_data=self.synchronized_data, - context=MagicMock(), - ) - late_arriving_tx_hashes = { - p: "".join(("1" * TX_HASH_LENGTH, "2" * TX_HASH_LENGTH)) - for p in self.participants - } - test_round.required_block_confirmations = 0 - self._complete_run( - self._test_round( - test_round=test_round, - round_payloads=get_participant_to_late_arriving_tx_hashes( - self.participants - ), - synchronized_data_update_fn=lambda _synchronized_data, _: _synchronized_data.update( - late_arriving_tx_hashes=late_arriving_tx_hashes, - suspects=tuple() - if expected_event == TransactionSettlementEvent.DONE - else tuple(sorted(late_arriving_tx_hashes.keys())), - ), - synchronized_data_attr_checks=[ - lambda _synchronized_data: _synchronized_data.late_arriving_tx_hashes, - lambda _synchronized_data: _synchronized_data.suspects, - ], - exit_event=expected_event, - ) - ) - - @pytest.mark.parametrize("correct_serialization", (True, False)) - def test_check_payload(self, correct_serialization: bool) -> None: - """Test the `check_payload` method.""" - - test_round = SynchronizeLateMessagesRound( - synchronized_data=self.synchronized_data, - context=MagicMock(), - ) - sender = list(test_round.accepting_payloads_from).pop() - hash_length = TX_HASH_LENGTH - if not correct_serialization: - hash_length -= 1 - tx_hashes = "0" * hash_length - payload = SynchronizeLateMessagesPayload(sender=sender, tx_hashes=tx_hashes) - - if correct_serialization: - test_round.check_payload(payload) - return - - with pytest.raises( - TransactionNotValidError, match="Expecting serialized data of chunk size" - ): - test_round.check_payload(payload) - - with pytest.raises( - ABCIAppInternalError, match="Expecting serialized data of chunk size" - ): - test_round.process_payload(payload) - assert payload not in test_round.collection - - -def test_synchronized_datas() -> None: - """Test SynchronizedData.""" - - participants = get_participants() - participant_to_randomness = get_participant_to_randomness(participants, 1) - participant_to_randomness_serialized = CollectionRound.serialize_collection( - participant_to_randomness - ) - most_voted_randomness = get_most_voted_randomness() - participant_to_selection = get_participant_to_selection(participants, "test") - participant_to_selection_serialized = CollectionRound.serialize_collection( - participant_to_selection - ) - safe_contract_address = get_safe_contract_address() - most_voted_tx_hash = get_most_voted_tx_hash() - participant_to_signature = get_participant_to_signature(participants) - participant_to_signature_serialized = CollectionRound.serialize_collection( - participant_to_signature - ) - final_tx_hash = get_final_tx_hash() - actual_keeper_randomness = int(most_voted_randomness, base=16) / MAX_INT_256 - late_arriving_tx_hashes_serialized = get_late_arriving_tx_hashes_serialized() - late_arriving_tx_hashes_deserialized = get_late_arriving_tx_hashes_deserialized() - keepers = get_keepers(deque(("agent_1" + "-" * 35, "agent_3" + "-" * 35))) - expected_keepers = deque(["agent_1" + "-" * 35, "agent_3" + "-" * 35]) - - # test `keeper_retries` property when no `keepers` are set. - synchronized_data_____ = TransactionSettlementSynchronizedSata( - AbciAppDB(setup_data=dict()) - ) - assert synchronized_data_____.keepers == deque() - assert synchronized_data_____.keeper_retries == 0 - - synchronized_data_____ = TransactionSettlementSynchronizedSata( - AbciAppDB( - setup_data=AbciAppDB.data_to_lists( - dict( - all_participants=tuple(participants), - participants=tuple(participants), - consensus_threshold=3, - participant_to_randomness=participant_to_randomness_serialized, - most_voted_randomness=most_voted_randomness, - participant_to_selection=participant_to_selection_serialized, - safe_contract_address=safe_contract_address, - most_voted_tx_hash=most_voted_tx_hash, - participant_to_signature=participant_to_signature_serialized, - final_tx_hash=final_tx_hash, - late_arriving_tx_hashes=late_arriving_tx_hashes_serialized, - keepers=keepers, - blacklisted_keepers="t" * 42, - ) - ), - ) - ) - assert ( - abs(synchronized_data_____.keeper_randomness - actual_keeper_randomness) < 1e-10 - ) # avoid equality comparisons between floats - assert synchronized_data_____.most_voted_randomness == most_voted_randomness - assert synchronized_data_____.safe_contract_address == safe_contract_address - assert synchronized_data_____.most_voted_tx_hash == most_voted_tx_hash - assert synchronized_data_____.participant_to_randomness == participant_to_randomness - assert synchronized_data_____.participant_to_selection == participant_to_selection - assert synchronized_data_____.participant_to_signature == participant_to_signature - assert synchronized_data_____.final_tx_hash == final_tx_hash - assert ( - synchronized_data_____.late_arriving_tx_hashes - == late_arriving_tx_hashes_deserialized - ) - assert synchronized_data_____.keepers == expected_keepers - assert synchronized_data_____.keeper_retries == 1 - assert ( - synchronized_data_____.most_voted_keeper_address == expected_keepers.popleft() - ) - assert synchronized_data_____.keepers_threshold_exceeded - assert synchronized_data_____.blacklisted_keepers == {"t" * 42} - updated_synchronized_data = synchronized_data_____.create() - assert updated_synchronized_data.blacklisted_keepers == set() - - -class TestResetRound(BaseCollectSameUntilThresholdRoundTest): - """Test ResetRound.""" - - _synchronized_data_class = TransactionSettlementSynchronizedSata - _event_class = TransactionSettlementEvent - - def test_runs( - self, - ) -> None: - """Runs tests.""" - randomness = DUMMY_RANDOMNESS - synchronized_data = self.synchronized_data.update( - most_voted_randomness=randomness, - late_arriving_tx_hashes={}, - keepers="", - ) - synchronized_data._db._cross_period_persisted_keys = frozenset( - {"most_voted_randomness"} - ) - test_round = ResetRound( - synchronized_data=synchronized_data, - context=MagicMock(), - ) - next_period_count = 1 - self._complete_run( - self._test_round( - test_round=test_round, - round_payloads=get_participant_to_period_count( - self.participants, next_period_count - ), - synchronized_data_update_fn=lambda _synchronized_data, _: _synchronized_data.create(), - synchronized_data_attr_checks=[], # [lambda _synchronized_data: _synchronized_data.participants], - most_voted_payload=next_period_count, - exit_event=self._event_class.DONE, - ) - ) diff --git a/packages/valory/skills/transaction_settlement_abci/tests/test_tools/__init__.py b/packages/valory/skills/transaction_settlement_abci/tests/test_tools/__init__.py deleted file mode 100644 index d1ae9fb..0000000 --- a/packages/valory/skills/transaction_settlement_abci/tests/test_tools/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2022-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Package for `test_tools` testing.""" diff --git a/packages/valory/skills/transaction_settlement_abci/tests/test_tools/test_integration.py b/packages/valory/skills/transaction_settlement_abci/tests/test_tools/test_integration.py deleted file mode 100644 index 84a6792..0000000 --- a/packages/valory/skills/transaction_settlement_abci/tests/test_tools/test_integration.py +++ /dev/null @@ -1,214 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2022-2023 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test transaction settlement integration test tool.""" - -from pathlib import Path -from typing import cast -from unittest import mock - -import pytest -from aea.exceptions import AEAActException -from web3.types import Nonce, Wei - -from packages.valory.protocols.contract_api import ContractApiMessage -from packages.valory.protocols.ledger_api import LedgerApiMessage -from packages.valory.skills import transaction_settlement_abci -from packages.valory.skills.abstract_round_abci.base import AbciAppDB -from packages.valory.skills.abstract_round_abci.tests.test_tools.base import ( - FSMBehaviourTestToolSetup, -) -from packages.valory.skills.transaction_settlement_abci.behaviours import ( - FinalizeBehaviour, -) -from packages.valory.skills.transaction_settlement_abci.rounds import ( - SynchronizedData as TxSettlementSynchronizedSata, -) -from packages.valory.skills.transaction_settlement_abci.test_tools.integration import ( - _GnosisHelperIntegration, - _SafeConfiguredHelperIntegration, - _TxHelperIntegration, -) - - -DUMMY_TX_HASH = "a" * 234 - - -class Test_SafeConfiguredHelperIntegration(FSMBehaviourTestToolSetup): - """Test_SafeConfiguredHelperIntegration""" - - test_cls = _SafeConfiguredHelperIntegration - - def test_instantiation(self) -> None: - """Test instantiation""" - - self.set_path_to_skill() - self.test_cls.make_ledger_api_connection_callable = ( - lambda *_, **__: mock.MagicMock() - ) - test_instance = cast(_SafeConfiguredHelperIntegration, self.setup_test_cls()) - - assert test_instance.keeper_address in test_instance.safe_owners - - -class Test_GnosisHelperIntegration(FSMBehaviourTestToolSetup): - """Test_SafeConfiguredHelperIntegration""" - - test_cls = _GnosisHelperIntegration - - def test_instantiation(self) -> None: - """Test instantiation""" - - self.set_path_to_skill() - self.test_cls.make_ledger_api_connection_callable = ( - lambda *_, **__: mock.MagicMock() - ) - test_instance = cast(_GnosisHelperIntegration, self.setup_test_cls()) - - assert test_instance.safe_contract_address - assert test_instance.gnosis_instance - assert test_instance.ethereum_api - - -class Test_TxHelperIntegration(FSMBehaviourTestToolSetup): - """Test_SafeConfiguredHelperIntegration""" - - test_cls = _TxHelperIntegration - - def instantiate_test(self) -> _TxHelperIntegration: - """Instantiate the test""" - - path_to_skill = Path(transaction_settlement_abci.__file__).parent - self.set_path_to_skill(path_to_skill=path_to_skill) - self.test_cls.make_ledger_api_connection_callable = ( - lambda *_, **__: mock.MagicMock() - ) - - db = AbciAppDB( - setup_data={"all_participants": [f"agent_{i}" for i in range(4)]} - ) - self.test_cls.tx_settlement_synchronized_data = TxSettlementSynchronizedSata(db) - - test_instance = cast(_TxHelperIntegration, self.setup_test_cls()) - return test_instance - - def test_sign_tx(self) -> None: - """Test sign_tx""" - - test_instance = self.instantiate_test() - test_instance.tx_settlement_synchronized_data.db.update( - most_voted_tx_hash=DUMMY_TX_HASH - ) - - target = test_instance.gnosis_instance.functions.getOwners - return_value = test_instance.safe_owners - with mock.patch.object( - target, "call", new_callable=lambda: lambda: return_value - ): - test_instance.sign_tx() - - def test_sign_tx_failure(self) -> None: - """Test sign_tx failure""" - - test_instance = self.instantiate_test() - test_instance.tx_settlement_synchronized_data.db.update( - most_voted_tx_hash=DUMMY_TX_HASH - ) - - target = test_instance.gnosis_instance.functions.getOwners - with mock.patch.object(target, "call", new_callable=lambda: lambda: {}): - with pytest.raises(AssertionError): - test_instance.sign_tx() - - def test_send_tx(self) -> None: - """Test send tx""" - - test_instance = self.instantiate_test() - - nonce = Nonce(0) - gas_price = {"maxPriorityFeePerGas": Wei(0), "maxFeePerGas": Wei(0)} - behaviour = cast(FinalizeBehaviour, test_instance.behaviour.current_behaviour) - behaviour.params.mutable_params.gas_price = gas_price - behaviour.params.mutable_params.nonce = nonce - - contract_api_message = ContractApiMessage( - performative=ContractApiMessage.Performative.RAW_TRANSACTION, # type: ignore - raw_transaction=ContractApiMessage.RawTransaction( - ledger_id="", body={"nonce": str(nonce), **gas_price} - ), - ) - - ledger_api_message = LedgerApiMessage( - performative=LedgerApiMessage.Performative.TRANSACTION_DIGEST, # type: ignore - transaction_digest=LedgerApiMessage.TransactionDigest( - ledger_id="", body="" - ), - ) - - return_value = contract_api_message, None, ledger_api_message - - with mock.patch.object( - test_instance, - "process_n_messages", - new_callable=lambda: lambda *x, **__: return_value, - ): - test_instance.send_tx() - - def test_validate_tx(self) -> None: - """Test validate_tx""" - - test_instance = self.instantiate_test() - test_instance.tx_settlement_synchronized_data.db.update( - tx_hashes_history="a" * 64 - ) - - contract_api_message = ContractApiMessage( - performative=ContractApiMessage.Performative.STATE, # type: ignore - state=ContractApiMessage.State( - ledger_id="", - body={"verified": True}, - ), - ) - return_value = None, contract_api_message - - with mock.patch.object( - test_instance, - "process_n_messages", - new_callable=lambda: lambda *x, **__: return_value, - ): - test_instance.validate_tx() - - def test_validate_tx_timeout(self) -> None: - """Test validate_tx timeout""" - - test_instance = self.instantiate_test() - synchronized_data = test_instance.tx_settlement_synchronized_data - assert synchronized_data.n_missed_messages == 0 - test_instance.validate_tx(simulate_timeout=True) - assert synchronized_data.n_missed_messages == 1 - - def test_validate_tx_failure(self) -> None: - """Test validate tx failure""" - - test_instance = self.instantiate_test() - - with pytest.raises( - AEAActException, match="FSM design error: tx hash should exist" - ): - test_instance.validate_tx() From c777f1cbdf62fe0485a0e898174d6f4002c765a0 Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Tue, 22 Oct 2024 19:32:23 +0530 Subject: [PATCH 13/41] chore: Update agent_transition environment variable in config files --- .../valory/agents/optimus/aea-config.yaml | 70 +++++-------------- .../skills/liquidity_trader_abci/skill.yaml | 10 +-- .../valory/skills/optimus_abci/skill.yaml | 25 +++---- 3 files changed, 34 insertions(+), 71 deletions(-) diff --git a/packages/valory/agents/optimus/aea-config.yaml b/packages/valory/agents/optimus/aea-config.yaml index 15bf0c9..40bd25f 100644 --- a/packages/valory/agents/optimus/aea-config.yaml +++ b/packages/valory/agents/optimus/aea-config.yaml @@ -8,7 +8,7 @@ fingerprint: __init__.py: bafybeigx5mdvnamsqfum5ut7htok2y5vsnu7lrvms5gfvqi7hmv7sfbo3a fingerprint_ignore_patterns: [] connections: -- eightballer/dcxt:0.1.0:bafybeide6f32midzxzo7ms7xn3xokpthxbxwqzmuiarpl5r4guhjrp623q +- eightballer/dcxt:0.1.0:bafybeihes2id6qmqqybvdqfpchu6h3i5mpnhq6lztfxp52twd22iwqxacq - valory/abci:0.1.0:bafybeiejymu4ul62zx6weoibnlsrfprfpjnplhjefz6sr6izgdr4sajlnu - valory/http_client:0.23.0:bafybeihi772xgzpqeipp3fhmvpct4y6e6tpjp4sogwqrnf3wqspgeilg4u - valory/http_server:0.22.0:bafybeihpgu56ovmq4npazdbh6y6ru5i7zuv6wvdglpxavsckyih56smu7m @@ -37,17 +37,17 @@ protocols: skills: - valory/abstract_abci:0.1.0:bafybeidz54kvxhbdmpruzguuzzq7bjg4pekjb5amqobkxoy4oqknnobopu - valory/abstract_round_abci:0.1.0:bafybeiajjzuh6vf23crp55humonknirvv2f4s3dmdlfzch6tc5ow52pcgm -- valory/liquidity_trader_abci:0.1.0:bafybeihtca6gtyjibj6wkrcdmx3fb3a3bkpdgsphwevkatagxrbqvh6fd4 -- valory/market_data_fetcher_abci:0.1.0:bafybeieyaop63uqw3nk2mx7nu3yvqp45eioz7rkfn5n3ocvvt3odrddoke -- valory/strategy_evaluator_abci:0.1.0:bafybeihc7pxnwmgj2wrf7awdeiu2yjyvudlmdyfkahmpkiwq7dyt5aa44u -- valory/optimus_abci:0.1.0:bafybeifjpvqz2m7qhztib4xcjpbjkuiutrot22flqclg36amvqvrp5ra3e +- valory/liquidity_trader_abci:0.1.0:bafybeidccg5fwmxcdoo6llhxbyqwtxjayvz7byd5c4h6q7fsqkma4uioru +- valory/market_data_fetcher_abci:0.1.0:bafybeiffexwacktnhihmnhxdqs2msdzbigth62oqb7ghe2bqxwfkyvs5ty +- valory/strategy_evaluator_abci:0.1.0:bafybeibdq4s53qakp55lagj6i2aut5vvqzidcvmo6bnqezie6ki7uzqnta +- valory/optimus_abci:0.1.0:bafybeialrii6l3bhz3zbij4fm7ls5bgjvavhahfxbgg3kfktrjnmlxoqrq - valory/registration_abci:0.1.0:bafybeiffipsowrqrkhjoexem7ern5ob4fabgif7wa6gtlszcoaop2e3oey - valory/reset_pause_abci:0.1.0:bafybeif4lgvbzsmzljesxbphycdv52ka7qnihyjrjpfaseclxadcmm6yiq - valory/termination_abci:0.1.0:bafybeiekkpo5qef5zaeagm3si6v45qxcojvtjqe4a5ceccvk4q7k3xi3bi - valory/transaction_settlement_abci:0.1.0:bafybeielv6eivt2z6nforq43xewl2vmpfwpdu2s2vfogobziljnwsclmlm -- valory/trader_decision_maker_abci:0.1.0:bafybeih7duxvbjevmeez4bvdpahgvefoik3hnpjq3ik5g4jaqbuw5rybtu -- valory/ipfs_package_downloader:0.1.0:bafybeid54srronvfqbvcdjgtuhmr4mbndjkpxtgzguykeg4p3wwj3zboyi -- valory/portfolio_tracker_abci:0.1.0:bafybeictj7o35cttmhy43xi25fxyqmfhb7g2rd4yefj6hq2362xukifrpi +- valory/trader_decision_maker_abci:0.1.0:bafybeicskzcqr7dd4d5ulwss5u3hsx2wvcxatugtxiuja2neobbr3tqtkm +- valory/ipfs_package_downloader:0.1.0:bafybeieokinjosnulrsee3kbj7ly4kjfx2ub6lmwkyplgs33vxgmx3fbvm +- valory/portfolio_tracker_abci:0.1.0:bafybeiay2lasy2mtp5fxd77wgn2ocgy5neg42mlyitltuzrr7b6yzbduoa default_ledger: ethereum required_ledgers: - ethereum @@ -149,7 +149,7 @@ type: skill models: benchmark_tool: args: - log_dir: ${str:/logs} + log_dir: ${str:/Users/gauravlochab/repos/optimus/logs} get_balance: args: api_id: ${str:get_balance} @@ -163,7 +163,6 @@ models: error_type: ${str:str} retries: ${int:5} url: ${str:https://api.mainnet-beta.solana.com} - token_accounts: args: api_id: ${str:token_accounts} @@ -176,8 +175,7 @@ models: error_key: ${str:error:message} error_type: ${str:str} retries: ${int:5} - url: ${str:https://api.mainnet-beta.solana.com} - + url: ${str:https://api.mainnet-beta.solana.com} coingecko: args: endpoint: ${str:https://api.coingecko.com/api/v3/coins/{token_id}/market_chart?vs_currency=usd&days=1} @@ -203,8 +201,8 @@ models: response_key: ${str:null} response_type: ${str:dict} retries: ${int:5} - url: ${str:http://localhost:3000/tx} - params: #TO_DO: Add the params for the skill from babydegen + url: ${str:http://localhost:3000/tx} + params: args: cleanup_history_depth: 1 cleanup_history_depth_current: null @@ -244,7 +242,7 @@ models: service_id: optimus service_registry_address: ${str:null} setup: - all_participants: ${list:["0x1aCD50F973177f4D320913a9Cc494A9c66922fdF"]} + all_participants: ${list:["0x08012c56eD8adF43586A3cEf68EEb13FDfF70Ef5"]} consensus_threshold: ${int:null} safe_contract_address: ${str:0x0000000000000000000000000000000000000000} share_tm_config_on_startup: ${bool:false} @@ -296,12 +294,12 @@ models: tenderly_access_key: ${str:access_key} tenderly_account_slug: ${str:account_slug} tenderly_project_slug: ${str:project_slug} - agent_transition: ${str:agent_transition} + agent_transition: ${bool:true} chain_to_chain_id_mapping: ${str:{"optimism":10,"base":8453,"ethereum":1}} staking_token_contract_address: ${str:0x88996bbdE7f982D93214881756840cE2c77C4992} staking_activity_checker_contract_address: ${str:0x7Fd1F4b764fA41d19fe3f63C85d12bf64d2bbf68} staking_threshold_period: ${int:5} - store_path: ${str:/data/} + store_path: ${str:/Users/gauravlochab/repos/optimus/data/} assets_info_filename: ${str:assets.json} pool_info_filename: ${str:current_pool.json} gas_cost_info_filename: ${str:gas_costs.json} @@ -309,7 +307,6 @@ models: max_fee_percentage: ${float:0.02} max_gas_percentage: ${float:0.25} balancer_graphql_endpoints: ${str:{"optimism":"https://api.studio.thegraph.com/query/75376/balancer-optimism-v2/version/latest","base":"https://api.studio.thegraph.com/query/24660/balancer-base-v2/version/latest"}} - #on_chain_service_id: ${int:null} # null or 1 token_symbol_whitelist: ${list:["coingecko_id=weth&address=7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs"]} tracked_tokens: ${list:["7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs"]} strategies_kwargs: ${list:[["ma_period",35]]} @@ -331,41 +328,6 @@ models: - balancer trade_size_in_base_token: ${float:0.0001} --- -public_id: valory/p2p_libp2p_client:0.1.0 -type: connection -config: - nodes: - - uri: ${str:acn.staging.autonolas.tech:9005} - public_key: ${str:02d3a830c9d6ea1ae91936951430dee11f4662f33118b02190693be835359a9d77} -cert_requests: -- identifier: acn - ledger_id: ethereum - message_format: '{public_key}' - not_after: '2024-01-01' - not_before: '2023-01-01' - public_key: ${str:02d3a830c9d6ea1ae91936951430dee11f4662f33118b02190693be835359a9d77} - save_path: .certs/acn_cosmos_9005.txt ---- -public_id: valory/ledger:0.19.0 -type: connection -config: - ledger_apis: - ethereum: - address: ${str:https://base.blockpi.network/v1/rpc/public} - chain_id: ${int:8453} - default_gas_price_strategy: ${str:eip1559} - poa_chain: ${bool:false} - optimism: - address: ${str:https://mainnet.optimism.io} - chain_id: ${int:10} - default_gas_price_strategy: ${str:eip1559} - poa_chain: ${bool:false} - base: - address: ${str:https://base.meowrpc.com} - chain_id: ${int:8453} - default_gas_price_strategy: ${str:eip1559} - poa_chain: ${bool:false} ---- public_id: valory/http_client:0.23.0 type: connection config: @@ -382,4 +344,4 @@ config: key_path: ${str:ethereum_private_key.txt} ledger_id: ${str:base} rpc_url: ${str:https://base.blockpi.network/v1/rpc/public} - etherscan_api_key: ${str:YOUR_ETHERSCAN_API_KEY} \ No newline at end of file + etherscan_api_key: ${str:YOUR_ETHERSCAN_API_KEY} diff --git a/packages/valory/skills/liquidity_trader_abci/skill.yaml b/packages/valory/skills/liquidity_trader_abci/skill.yaml index eb519e8..7b91305 100644 --- a/packages/valory/skills/liquidity_trader_abci/skill.yaml +++ b/packages/valory/skills/liquidity_trader_abci/skill.yaml @@ -7,16 +7,16 @@ license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeia7bn2ahqqwkf63ptje6rfnftuwrsp33sswgpcbh5osbesxxr6g4m - behaviours.py: bafybeidjzfcuxv7nazzflbzwzb7qiihxkyjcvhx6762v6il6evert33epi + behaviours.py: bafybeigkt4ay2bydfntmtzadl5xtz3pdk2nvityudwyplc3n6xswhuhjwy dialogues.py: bafybeiay23otskx2go5xhtgdwfw2kd6rxd62sxxdu3njv7hageorl5zxzm fsm_specification.yaml: bafybeiabbiulb7k6xkjysulmy6o4ugnhxlpp5jiaeextvwj65q4ttadoeq handlers.py: bafybeidxw2lvgiifmo4siobpwuwbxscuifrdo3gnkjyn6bgexotj5f7zf4 - models.py: bafybeicfw4yk5atz456qt6urbkyhmav2lbchy3bu3rcv2zezmsqj2zxk3y - payloads.py: bafybeie5tbx3lqcklwfqdqft5t33oluvkltnbjqavhj3ddjjblocyq62qm + models.py: bafybeihhdgk5ui4qmcszamyaxthtgizrlmmbmlo2y6hu7taqis5u44favq + payloads.py: bafybeifl4vmex2tj4k4ysv65ds5kzdjpydusxoatph7avfsbebfuhnygvm pool_behaviour.py: bafybeiaheuesscgqzwjbpyrezgwpdbdfurlmfwbc462qv6rblwwxlx5dpm pools/balancer.py: bafybeigznhgv7ylo5dvlhxcqikhiuqlqtnx3ikv4tszyvkl2lpcuqgoa5u pools/uniswap.py: bafybeigmqptgmjaxscszohfusgxsexqyx4awuyw7p4g5l7k2qpeyq7vdcu - rounds.py: bafybeifncouov6hhpfx2yihtapl5lgn5rn3nojhifwsdlclrtgv7bnldfu + rounds.py: bafybeidm3pkkrokqpyb7ncjfvekaezpx57o2mmyc2qgcn7wagtmu5xsyqm strategies/simple_strategy.py: bafybeiasu2nchowx6leksjllpuum4ckezxoj4o2m4sstavblplvvutmvzm strategy_behaviour.py: bafybeidk6sorg47kuuubamcccksi65x3txldyo7y2hm5opbye2ghmz2ljy fingerprint_ignore_patterns: [] @@ -173,7 +173,7 @@ models: merkl_user_rewards_url: https://api.merkl.xyz/v3/userRewards tenderly_bundle_simulation_url: https://api.tenderly.co/api/v1/account/{tenderly_account_slug}/project/{tenderly_project_slug}/simulate-bundle tenderly_access_key: access_key - agent_transition: agent_transition + agent_transition: false tenderly_account_slug: account_slug tenderly_project_slug: project_slug chain_to_chain_id_mapping: '{"optimism":10,"base":8453,"ethereum":1}' diff --git a/packages/valory/skills/optimus_abci/skill.yaml b/packages/valory/skills/optimus_abci/skill.yaml index c85c5ec..0f7ec79 100644 --- a/packages/valory/skills/optimus_abci/skill.yaml +++ b/packages/valory/skills/optimus_abci/skill.yaml @@ -7,30 +7,30 @@ license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeiechr3zr5bc4xl3vvs2p2gti54ily7ao2gu4ff5lys6cixehzkdea - behaviours.py: bafybeig3agtm256pl7rekx3i3kbz6h5hrnneu44f4pzkqp4mwtdxuob54e - composition.py: bafybeigxpycqh7xnqwdj5eg2ln6kqchbwvwuvzjtpwhgylr6qb56ooo7wu - dialogues.py: bafybeiafoomno5pn6qrx43jxf2opxkil5eg4nod6jhd5oqwwplfz4x6dke + behaviours.py: bafybeidutyxryf6h7ooc7ggrdl5lojfv3anrvf3zq4tbwkiwxm7qqrriye + composition.py: bafybeiff7fikz76hrhfv6cgajfov3kb2ivkhkzmvhqzfnchqjtdxh6qb2u + dialogues.py: bafybeift7d4mk4qkzm4rqmdrfv43erhunzyqmwn6pecuumq25amy7q7fp4 fsm_specification.yaml: bafybeiehe6ps7xi7i7tu4mduid7vbvszhsiu5juyard24i3nhtqgljpcza handlers.py: bafybeife4nrwqiwrx2ucza3vk6t5inpkncuewehtdnitax4lmqq2ptoona - models.py: bafybeibxpw4zmigtel3emgbz2jz6xgsfx7eavxvhqrqftwcsul6lwcpjou + models.py: bafybeihwcvw363vsglhgtneklcihcngnqmegjzasjow3ftxkfwnopomhoe fingerprint_ignore_patterns: [] connections: [] contracts: [] -protocols: +protocols: - eightballer/tickers:0.1.0:bafybeicjbpa24tla2enenmlzipqhu6grutqso74q6y7is2cpk7acub3bca - eightballer/orders:0.1.0:bafybeibprhniaoq3y2uzc4arwwl7yws3i54ahaicrphh5gtl4xxhxqexdy -- eightballer/balances:0.1.0:bafybeieczloag3mjzvd4y2qqpbrtx6suoqjww3v7mf3dgrez5xopmo4h3m +- eightballer/balances:0.1.0:bafybeiajh5vzhcofdpemm3545t3yh6g4okpwnejvbqchxapo765batiitu skills: - valory/abstract_round_abci:0.1.0:bafybeiajjzuh6vf23crp55humonknirvv2f4s3dmdlfzch6tc5ow52pcgm - valory/registration_abci:0.1.0:bafybeiffipsowrqrkhjoexem7ern5ob4fabgif7wa6gtlszcoaop2e3oey - valory/reset_pause_abci:0.1.0:bafybeif4lgvbzsmzljesxbphycdv52ka7qnihyjrjpfaseclxadcmm6yiq - valory/termination_abci:0.1.0:bafybeiekkpo5qef5zaeagm3si6v45qxcojvtjqe4a5ceccvk4q7k3xi3bi -- valory/liquidity_trader_abci:0.1.0:bafybeihtca6gtyjibj6wkrcdmx3fb3a3bkpdgsphwevkatagxrbqvh6fd4 +- valory/liquidity_trader_abci:0.1.0:bafybeidccg5fwmxcdoo6llhxbyqwtxjayvz7byd5c4h6q7fsqkma4uioru - valory/transaction_settlement_abci:0.1.0:bafybeielv6eivt2z6nforq43xewl2vmpfwpdu2s2vfogobziljnwsclmlm -- valory/market_data_fetcher_abci:0.1.0:bafybeieyaop63uqw3nk2mx7nu3yvqp45eioz7rkfn5n3ocvvt3odrddoke -- valory/trader_decision_maker_abci:0.1.0:bafybeih7duxvbjevmeez4bvdpahgvefoik3hnpjq3ik5g4jaqbuw5rybtu -- valory/strategy_evaluator_abci:0.1.0:bafybeihc7pxnwmgj2wrf7awdeiu2yjyvudlmdyfkahmpkiwq7dyt5aa44u -- valory/portfolio_tracker_abci:0.1.0:bafybeictj7o35cttmhy43xi25fxyqmfhb7g2rd4yefj6hq2362xukifrpi +- valory/market_data_fetcher_abci:0.1.0:bafybeiffexwacktnhihmnhxdqs2msdzbigth62oqb7ghe2bqxwfkyvs5ty +- valory/trader_decision_maker_abci:0.1.0:bafybeicskzcqr7dd4d5ulwss5u3hsx2wvcxatugtxiuja2neobbr3tqtkm +- valory/strategy_evaluator_abci:0.1.0:bafybeibdq4s53qakp55lagj6i2aut5vvqzidcvmo6bnqezie6ki7uzqnta +- valory/portfolio_tracker_abci:0.1.0:bafybeiay2lasy2mtp5fxd77wgn2ocgy5neg42mlyitltuzrr7b6yzbduoa behaviours: main: args: {} @@ -183,6 +183,7 @@ models: min_swap_amount_threshold: 10 max_fee_percentage: 0.02 max_gas_percentage: 0.1 + agent_transition: false merkl_fetch_campaigns_args: '{"url":"https://api.merkl.xyz/v3/campaigns","creator":"","live":"true"}' balancer_graphql_endpoints: '{"optimism":"https://api.studio.thegraph.com/query/75376/balancer-optimism-v2/version/latest","base":"https://api.studio.thegraph.com/query/24660/balancer-base-v2/version/latest"}' token_symbol_whitelist: [] @@ -336,5 +337,5 @@ models: class_name: OrdersDialogues dependencies: open-aea-cli-ipfs: - version: ==1.55.0 + version: ==1.57.0 is_abstract: false From 762854c0302734609026d87306b78eaae48760fe Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Thu, 24 Oct 2024 18:35:36 +0530 Subject: [PATCH 14/41] chore: changes in rounds , behaviours and merge conflit resolution --- .../valory/agents/optimus/aea-config.yaml | 13 ++++--- .../liquidity_trader_abci/behaviours.py | 14 +++---- .../skills/liquidity_trader_abci/payloads.py | 4 +- .../skills/liquidity_trader_abci/rounds.py | 38 +++++++++---------- .../valory/skills/optimus_abci/composition.py | 6 +-- .../valory/skills/optimus_abci/handlers.py | 13 ++++++- packages/valory/skills/optimus_abci/models.py | 37 +++++++++++++++++- 7 files changed, 85 insertions(+), 40 deletions(-) diff --git a/packages/valory/agents/optimus/aea-config.yaml b/packages/valory/agents/optimus/aea-config.yaml index 40bd25f..7d1cd42 100644 --- a/packages/valory/agents/optimus/aea-config.yaml +++ b/packages/valory/agents/optimus/aea-config.yaml @@ -37,15 +37,15 @@ protocols: skills: - valory/abstract_abci:0.1.0:bafybeidz54kvxhbdmpruzguuzzq7bjg4pekjb5amqobkxoy4oqknnobopu - valory/abstract_round_abci:0.1.0:bafybeiajjzuh6vf23crp55humonknirvv2f4s3dmdlfzch6tc5ow52pcgm -- valory/liquidity_trader_abci:0.1.0:bafybeidccg5fwmxcdoo6llhxbyqwtxjayvz7byd5c4h6q7fsqkma4uioru +- valory/liquidity_trader_abci:0.1.0:bafybeihafrz72evtascc2q5f2thpvunmr7vec6gipvqp3upnhkvkbqqrnu - valory/market_data_fetcher_abci:0.1.0:bafybeiffexwacktnhihmnhxdqs2msdzbigth62oqb7ghe2bqxwfkyvs5ty -- valory/strategy_evaluator_abci:0.1.0:bafybeibdq4s53qakp55lagj6i2aut5vvqzidcvmo6bnqezie6ki7uzqnta -- valory/optimus_abci:0.1.0:bafybeialrii6l3bhz3zbij4fm7ls5bgjvavhahfxbgg3kfktrjnmlxoqrq +- valory/strategy_evaluator_abci:0.1.0:bafybeicsxyuh2xa33h5jogq2bqumo3c3vutpsi5nj7aarfxfmy3jrjvxqe +- valory/optimus_abci:0.1.0:bafybeidtf4zg7qkef5biruh4vlcql2vd2ide4yhm4oza2vp263crx54wy4 - valory/registration_abci:0.1.0:bafybeiffipsowrqrkhjoexem7ern5ob4fabgif7wa6gtlszcoaop2e3oey - valory/reset_pause_abci:0.1.0:bafybeif4lgvbzsmzljesxbphycdv52ka7qnihyjrjpfaseclxadcmm6yiq - valory/termination_abci:0.1.0:bafybeiekkpo5qef5zaeagm3si6v45qxcojvtjqe4a5ceccvk4q7k3xi3bi - valory/transaction_settlement_abci:0.1.0:bafybeielv6eivt2z6nforq43xewl2vmpfwpdu2s2vfogobziljnwsclmlm -- valory/trader_decision_maker_abci:0.1.0:bafybeicskzcqr7dd4d5ulwss5u3hsx2wvcxatugtxiuja2neobbr3tqtkm +- valory/trader_decision_maker_abci:0.1.0:bafybeigkks2o42hoemll7qxq7ulgidqynvjczmpa2ifuhnztltjf7p63b4 - valory/ipfs_package_downloader:0.1.0:bafybeieokinjosnulrsee3kbj7ly4kjfx2ub6lmwkyplgs33vxgmx3fbvm - valory/portfolio_tracker_abci:0.1.0:bafybeiay2lasy2mtp5fxd77wgn2ocgy5neg42mlyitltuzrr7b6yzbduoa default_ledger: ethereum @@ -101,7 +101,7 @@ config: poa_chain: ${bool:false} default_gas_price_strategy: ${str:eip1559} base: - address: ${str:https://virtual.base.rpc.tenderly.co/5d9c013b-879b-4f20-a6cc-e95dee0d109f} + address: ${str:https://virtual.base.rpc.tenderly.co/828d3af0-504a-47ed-bd0c-41fc71f15f74} chain_id: ${int:8453} poa_chain: ${bool:false} default_gas_price_strategy: ${str:eip1559} @@ -239,12 +239,13 @@ models: request_retry_delay: 1.0 request_timeout: 10.0 round_timeout_seconds: 30.0 + proxy_round_timeout_seconds: ${float:1200.0} service_id: optimus service_registry_address: ${str:null} setup: all_participants: ${list:["0x08012c56eD8adF43586A3cEf68EEb13FDfF70Ef5"]} consensus_threshold: ${int:null} - safe_contract_address: ${str:0x0000000000000000000000000000000000000000} + safe_contract_address: ${str:0xc4ed6F4B3059AD6aa985A9e47C133a45A39db66e} share_tm_config_on_startup: ${bool:false} sleep_time: 1 tendermint_check_sleep_delay: 3 diff --git a/packages/valory/skills/liquidity_trader_abci/behaviours.py b/packages/valory/skills/liquidity_trader_abci/behaviours.py index 07cfe10..d8ee1f4 100644 --- a/packages/valory/skills/liquidity_trader_abci/behaviours.py +++ b/packages/valory/skills/liquidity_trader_abci/behaviours.py @@ -91,8 +91,8 @@ PostTxSettlementRound, StakingState, SynchronizedData, - DecideAgentRound, - DecideAgentPayload, + DecideAgentStartingRound, + DecideAgentStartingPayload ) from packages.valory.skills.liquidity_trader_abci.strategies.simple_strategy import ( SimpleStrategyBehaviour, @@ -3356,10 +3356,10 @@ def fetch_and_log_gas_details(self): "Gas used or effective gas price not found in the response." ) -class DecideAgentBehaviour(LiquidityTraderBaseBehaviour): +class DecideAgentStartingBehaviour(LiquidityTraderBaseBehaviour): """Behaviour that executes all the actions.""" - matching_round: Type[AbstractRound] = DecideAgentRound + matching_round: Type[AbstractRound] = DecideAgentStartingRound def async_act(self) -> Generator: """Async act""" @@ -3370,7 +3370,7 @@ def async_act(self) -> Generator: else: next_event = Event.DONT_MOVE_TO_NEXT_AGENT.value - payload = DecideAgentPayload( + payload = DecideAgentStartingPayload( sender=sender, content=json.dumps( { @@ -3383,7 +3383,7 @@ def async_act(self) -> Generator: yield from self.send_a2a_transaction(payload) yield from self.wait_until_round_end() - self.set_done() + self.set_done() class LiquidityTraderRoundBehaviour(AbstractRoundBehaviour): """LiquidityTraderRoundBehaviour""" @@ -3396,7 +3396,7 @@ class LiquidityTraderRoundBehaviour(AbstractRoundBehaviour): GetPositionsBehaviour, EvaluateStrategyBehaviour, DecisionMakingBehaviour, - DecideAgentBehaviour, + DecideAgentStartingBehaviour, PostTxSettlementBehaviour, ] diff --git a/packages/valory/skills/liquidity_trader_abci/payloads.py b/packages/valory/skills/liquidity_trader_abci/payloads.py index 182122c..5e6d2c8 100644 --- a/packages/valory/skills/liquidity_trader_abci/payloads.py +++ b/packages/valory/skills/liquidity_trader_abci/payloads.py @@ -71,10 +71,10 @@ class DecisionMakingPayload(BaseTxPayload): content: str @dataclass(frozen=True) -class DecideAgentPayload(BaseTxPayload): +class DecideAgentStartingPayload(BaseTxPayload): """Represent a transaction payload for the DecideAgentRound.""" - content: str + content: str @dataclass(frozen=True) diff --git a/packages/valory/skills/liquidity_trader_abci/rounds.py b/packages/valory/skills/liquidity_trader_abci/rounds.py index 5da35d5..3ea6ac8 100644 --- a/packages/valory/skills/liquidity_trader_abci/rounds.py +++ b/packages/valory/skills/liquidity_trader_abci/rounds.py @@ -41,7 +41,7 @@ EvaluateStrategyPayload, GetPositionsPayload, PostTxSettlementPayload, - DecideAgentPayload, + DecideAgentStartingPayload ) @@ -397,10 +397,10 @@ def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: return self.synchronized_data, Event.NO_MAJORITY return None -class DecideAgentRound(CollectSameUntilThresholdRound): +class DecideAgentStartingRound(CollectSameUntilThresholdRound): """DecisionMakingRound""" - payload_class = DecisionMakingPayload + payload_class = DecideAgentStartingPayload synchronized_data_class = SynchronizedData done_event = Event.DONE @@ -419,7 +419,7 @@ def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: ): return self.synchronized_data, Event.NO_MAJORITY return None - + class PostTxSettlementRound(CollectSameUntilThresholdRound): """A round that will be called after tx settlement is done.""" @@ -468,11 +468,10 @@ class FinishedTxPreparationRound(DegenerateRound): class FailedMultiplexerRound(DegenerateRound): - """FailedMultiplexerRound""" - -class SwitchAgentRound(DegenerateRound): - """SwitchAgentRound""" + """FailedMultiplexerRound""" +class SwitchAgentStartingRound(DegenerateRound): + """SwitchAgentRound""" class LiquidityTraderAbciApp(AbciApp[Event]): """LiquidityTraderAbciApp""" @@ -480,6 +479,7 @@ class LiquidityTraderAbciApp(AbciApp[Event]): initial_round_cls: AppState = CallCheckpointRound initial_states: Set[AppState] = { CallCheckpointRound, + DecideAgentStartingRound, CheckStakingKPIMetRound, GetPositionsRound, DecisionMakingRound, @@ -513,22 +513,21 @@ class LiquidityTraderAbciApp(AbciApp[Event]): Event.DONE: DecisionMakingRound, Event.NO_MAJORITY: EvaluateStrategyRound, Event.ROUND_TIMEOUT: EvaluateStrategyRound, - Event.WAIT: DecideAgentRound, + Event.WAIT: FinishedEvaluateStrategyRound, }, DecisionMakingRound: { - Event.DONE: DecideAgentRound, + Event.DONE: FinishedDecisionMakingRound, Event.ERROR: FinishedDecisionMakingRound, Event.NO_MAJORITY: DecisionMakingRound, Event.ROUND_TIMEOUT: DecisionMakingRound, Event.SETTLE: FinishedTxPreparationRound, Event.UPDATE: DecisionMakingRound, }, - DecideAgentRound: { - Event.DONT_MOVE_TO_NEXT_AGENT: FinishedDecisionMakingRound, - Event.MOVE_TO_NEXT_AGENT: SwitchAgentRound, - Event.DONE: FinishedDecisionMakingRound, - Event.NO_MAJORITY: FinishedDecisionMakingRound, - + DecideAgentStartingRound: { + Event.DONT_MOVE_TO_NEXT_AGENT: CallCheckpointRound, + Event.MOVE_TO_NEXT_AGENT: SwitchAgentStartingRound, + Event.DONE: CallCheckpointRound, + Event.NO_MAJORITY: CallCheckpointRound, }, PostTxSettlementRound: { Event.ACTION_EXECUTED: DecisionMakingRound, @@ -540,7 +539,7 @@ class LiquidityTraderAbciApp(AbciApp[Event]): FinishedEvaluateStrategyRound: {}, FinishedTxPreparationRound: {}, FinishedDecisionMakingRound: {}, - SwitchAgentRound:{}, + SwitchAgentStartingRound:{}, FinishedCallCheckpointRound: {}, FinishedCheckStakingKPIMetRound: {}, FailedMultiplexerRound: {}, @@ -548,7 +547,7 @@ class LiquidityTraderAbciApp(AbciApp[Event]): final_states: Set[AppState] = { FinishedEvaluateStrategyRound, FinishedDecisionMakingRound, - SwitchAgentRound, + SwitchAgentStartingRound, FinishedTxPreparationRound, FinishedCallCheckpointRound, FinishedCheckStakingKPIMetRound, @@ -569,6 +568,7 @@ class LiquidityTraderAbciApp(AbciApp[Event]): CallCheckpointRound: set(), CheckStakingKPIMetRound: set(), GetPositionsRound: set(), + DecideAgentStartingRound: set(), DecisionMakingRound: set(), PostTxSettlementRound: set(), } @@ -580,6 +580,6 @@ class LiquidityTraderAbciApp(AbciApp[Event]): FailedMultiplexerRound: set(), FinishedEvaluateStrategyRound: set(), FinishedDecisionMakingRound: set(), - SwitchAgentRound: set(), + SwitchAgentStartingRound: set(), FinishedTxPreparationRound: {get_name(SynchronizedData.most_voted_tx_hash)}, } diff --git a/packages/valory/skills/optimus_abci/composition.py b/packages/valory/skills/optimus_abci/composition.py index 5647326..26e66c6 100644 --- a/packages/valory/skills/optimus_abci/composition.py +++ b/packages/valory/skills/optimus_abci/composition.py @@ -40,10 +40,10 @@ abci_app_transition_mapping: AbciAppTransitionMapping = { - RegistrationAbci.FinishedRegistrationRound: LiquidityTraderAbci.CallCheckpointRound, + RegistrationAbci.FinishedRegistrationRound: LiquidityTraderAbci.DecideAgentStartingRound, LiquidityTraderAbci.FinishedCallCheckpointRound: TransactionSettlementAbci.RandomnessTransactionSubmissionRound, LiquidityTraderAbci.FinishedCheckStakingKPIMetRound: TransactionSettlementAbci.RandomnessTransactionSubmissionRound, - LiquidityTraderAbci.SwitchAgentRound:TraderDecisionMakerAbci.RandomnessRound, + LiquidityTraderAbci.SwitchAgentStartingRound:TraderDecisionMakerAbci.RandomnessRound, TraderDecisionMakerAbci.FinishedTraderDecisionMakerRound: MarketDataFetcherAbci.FetchMarketDataRound, TraderDecisionMakerAbci.FailedTraderDecisionMakerRound: TraderDecisionMakerAbci.RandomnessRound, MarketDataFetcherAbci.FinishedMarketFetchRound: PortfolioTrackerAbci.PortfolioTrackerRound, @@ -63,7 +63,7 @@ LiquidityTraderAbci.FailedMultiplexerRound: ResetAndPauseAbci.ResetAndPauseRound, TransactionSettlementAbci.FinishedTransactionSubmissionRound: LiquidityTraderAbci.PostTxSettlementRound, TransactionSettlementAbci.FailedRound: ResetAndPauseAbci.ResetAndPauseRound, - ResetAndPauseAbci.FinishedResetAndPauseRound: LiquidityTraderAbci.CallCheckpointRound, + ResetAndPauseAbci.FinishedResetAndPauseRound: LiquidityTraderAbci.DecideAgentStartingRound, ResetAndPauseAbci.FinishedResetAndPauseErrorRound: RegistrationAbci.RegistrationRound, } diff --git a/packages/valory/skills/optimus_abci/handlers.py b/packages/valory/skills/optimus_abci/handlers.py index e53e8e1..4d640e3 100644 --- a/packages/valory/skills/optimus_abci/handlers.py +++ b/packages/valory/skills/optimus_abci/handlers.py @@ -40,7 +40,15 @@ from packages.valory.skills.abstract_round_abci.handlers import ( TendermintHandler as BaseTendermintHandler, ) - +from packages.valory.skills.market_data_fetcher_abci.handlers import ( + DcxtTickersHandler as BaseDcxtTickersHandler, +) +from packages.valory.skills.portfolio_tracker_abci.handlers import ( + DcxtBalancesHandler as BaseDcxtBalancesHandler, +) +from packages.valory.skills.strategy_evaluator_abci.handlers import ( + DcxtOrdersHandler as BaseDcxtOrdersHandler, +) ABCIHandler = BaseABCIRoundHandler HttpHandler = BaseHttpHandler @@ -49,3 +57,6 @@ ContractApiHandler = BaseContractApiHandler TendermintHandler = BaseTendermintHandler IpfsHandler = BaseIpfsHandler +DcxtTickersHandler = BaseDcxtTickersHandler +DcxtBalancesHandler = BaseDcxtBalancesHandler +DcxtOrdersHandler = BaseDcxtOrdersHandler diff --git a/packages/valory/skills/optimus_abci/models.py b/packages/valory/skills/optimus_abci/models.py index 6daaefc..5c8d407 100644 --- a/packages/valory/skills/optimus_abci/models.py +++ b/packages/valory/skills/optimus_abci/models.py @@ -52,13 +52,40 @@ TxSettlementProxy, ) +from packages.valory.skills.transaction_settlement_abci.models import TransactionParams +from packages.valory.skills.strategy_evaluator_abci.models import ( + StrategyEvaluatorParams as StrategyEvaluatorParams, +) +from packages.valory.skills.market_data_fetcher_abci.models import ( + Params as MarketDataFetcherParams, +) +from packages.valory.skills.portfolio_tracker_abci.models import ( + Params as PortfolioTrackerParams, +) +from packages.valory.skills.trader_decision_maker_abci.models import ( + Params as TraderDecisionMakerParams, +) + +from packages.valory.skills.market_data_fetcher_abci.rounds import ( + Event as MarketDataFetcherEvent, +) +from packages.valory.skills.trader_decision_maker_abci.rounds import ( + Event as DecisionMakingEvent, +) +from packages.valory.skills.strategy_evaluator_abci.rounds import ( + Event as StrategyEvaluatorEvent, +) + EventType = Union[ Type[LiquidityTraderEvent], Type[TransactionSettlementEvent], Type[ResetPauseEvent], + Type[MarketDataFetcherEvent], + Type[DecisionMakingEvent], + Type[StrategyEvaluatorEvent], ] EventToTimeoutMappingType = Dict[ - Union[LiquidityTraderEvent, TransactionSettlementEvent, ResetPauseEvent], + Union[LiquidityTraderEvent, TransactionSettlementEvent, ResetPauseEvent, MarketDataFetcherEvent, DecisionMakingEvent, StrategyEvaluatorEvent], float, ] @@ -80,6 +107,11 @@ class Params( # pylint: disable=too-many-ancestors TerminationParams, LiquidityTraderParams, + TraderDecisionMakerParams, + MarketDataFetcherParams, + StrategyEvaluatorParams, + PortfolioTrackerParams, + TransactionParams, ): """A model to represent params for multiple abci apps.""" @@ -98,7 +130,7 @@ def setup(self) -> None: """Set up.""" super().setup() - events = (LiquidityTraderEvent, TransactionSettlementEvent, ResetPauseEvent) + events = (LiquidityTraderEvent, TransactionSettlementEvent, ResetPauseEvent, MarketDataFetcherEvent, DecisionMakingEvent, StrategyEvaluatorEvent) round_timeout = self.params.round_timeout_seconds round_timeout_overrides = { cast(EventType, event).ROUND_TIMEOUT: round_timeout for event in events @@ -111,6 +143,7 @@ def setup(self) -> None: LiquidityTraderEvent.ROUND_TIMEOUT: self.params.round_timeout_seconds * MULTIPLIER, ResetPauseEvent.RESET_AND_PAUSE_TIMEOUT: reset_pause_timeout, + StrategyEvaluatorEvent.PROXY_SWAP_TIMEOUT: self.params.proxy_round_timeout_seconds, } for event, override in event_to_timeout_overrides.items(): From 6875318919778c4ae8b088376c51ba9fc0902970 Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Thu, 24 Oct 2024 19:51:55 +0530 Subject: [PATCH 15/41] chore: Update packages --- packages/packages.json | 54 ++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/packages/packages.json b/packages/packages.json index 02e1203..c989985 100644 --- a/packages/packages.json +++ b/packages/packages.json @@ -1,10 +1,5 @@ { "dev": { - "custom/eightballer/rsi_strategy/0.1.0": "bafybeigbofp2nqwcxu3rlkuugpc3w6ils3u7glse7c335rddcqg56ybh34", - "custom/eightballer/sma_strategy/0.1.0": "bafybeibve7aw6oye6dc66nl2w6wxuqgxrfh5rilw64vvtfjbld6ocnbcr4", - "custom/eightballer/vwap_momentum/0.1.0": "bafybeih2uiklkrin777kzhyk5khlnj35wapw2m5zeojrhglfosm76xjhym", - "custom/valory/trend_following_strategy/0.1.0": "bafybeibejqrhpxpcszjjq6sqqknk6ja72zfiyebqmpyw32e5hjd5hanx7e", - "custom/eightballer/always_buy/0.1.0": "bafybeic2fpf5ozhkf5jgzmppmfsprqw5ayfx6spgl3owuws464n7mkhpqi", "contract/valory/balancer_weighted_pool/0.1.0": "bafybeidyjlrlq3jrbackewedwt5irokhjupxgpqfgur2ri426cap2oqt7a", "contract/valory/balancer_vault/0.1.0": "bafybeie6twptrkqddget7pjijzob2c4jqmrrtpkwombneh35xx56djz4ru", "contract/valory/erc20/0.1.0": "bafybeiav4gh7lxfnwp4f7oorkbvjxrdsgjgyhl43rgbblaugtl76zlx7vy", @@ -13,20 +8,17 @@ "contract/valory/merkl_distributor/0.1.0": "bafybeihaqsvmncuzmwv2r6iuzc5t7ur6ugdhephz7ydftypksjidpsylbq", "contract/valory/staking_token/0.1.0": "bafybeifrvtkofw5c26b3irm6izqfdpik6vpjhm6hqwcdzx333h6vhdanai", "contract/valory/staking_activity_checker/0.1.0": "bafybeibjzsi2r5b6xd4iwl4wbwldptnynryzsdpifym4mkv32ynswx22ou", - "skill/valory/liquidity_trader_abci/0.1.0": "bafybeibchfda234wvkiqayx7w2cnpxwfhmz3tct54cttp5ui6x65ppayzu", - "skill/valory/optimus_abci/0.1.0": "bafybeidrpmjnuu5xlfjktthv47rknpsrezmljlbtlrzvku5w34a2f2eakq", - "skill/valory/trader_decision_maker_abci/0.1.0": "bafybeicskzcqr7dd4d5ulwss5u3hsx2wvcxatugtxiuja2neobbr3tqtkm", - "skill/valory/strategy_evaluator_abci/0.1.0": "bafybeibdq4s53qakp55lagj6i2aut5vvqzidcvmo6bnqezie6ki7uzqnta", - "skill/valory/market_data_fetcher_abci/0.1.0": "bafybeiffexwacktnhihmnhxdqs2msdzbigth62oqb7ghe2bqxwfkyvs5ty", - "skill/valory/trader_abci/0.1.0": "bafybeig7vluejd62szp236nzhhgaaqbhcps2qgjhz2rmwjw2hijck2bfvm", - "skill/valory/ipfs_package_downloader/0.1.0": "bafybeieokinjosnulrsee3kbj7ly4kjfx2ub6lmwkyplgs33vxgmx3fbvm", - "skill/valory/portfolio_tracker_abci/0.1.0": "bafybeiay2lasy2mtp5fxd77wgn2ocgy5neg42mlyitltuzrr7b6yzbduoa", - "agent/valory/optimus/0.1.0": "bafybeidsw5dfduvt4gcgaaaxkw2k43ivgceumjgody3f257wsrbdpe25mi", - "agent/valory/solana_trader/0.1.0": "bafybeiei6g2i7bntofmpduy75tuulcleewmdiww5poxszj7yohv2wd63cq", - "service/valory/optimus/0.1.0": "bafybeiedmye7zypqrn6jrw6iyigwommbpdt5w6mvbvt7cgxk3otlntapli", - "service/valory/solana_trader/0.1.0": "bafybeib5tasy5wc3cjbb6k42gz4gx3ub43cd67f66iay2fkxxcuxmnuqpy" + "skill/valory/liquidity_trader_abci/0.1.0": "bafybeihafrz72evtascc2q5f2thpvunmr7vec6gipvqp3upnhkvkbqqrnu", + "skill/valory/optimus_abci/0.1.0": "bafybeiarvc5dcslcpjzatygne44jmhegvhzduzyf3gv65ih63zg77f4pp4", + "agent/valory/optimus/0.1.0": "bafybeicg2uhzgbybz64gdxm226mgg64htzxycnr7godrl7guk36da22cyy", + "service/valory/optimus/0.1.0": "bafybeihwqhbbepwhtgeaf2ysuf4dqdx6o3krj7tj74eh2jc2scwnffrooe" }, "third_party": { + "custom/eightballer/rsi_strategy/0.1.0": "bafybeigbofp2nqwcxu3rlkuugpc3w6ils3u7glse7c335rddcqg56ybh34", + "custom/eightballer/sma_strategy/0.1.0": "bafybeibve7aw6oye6dc66nl2w6wxuqgxrfh5rilw64vvtfjbld6ocnbcr4", + "custom/eightballer/vwap_momentum/0.1.0": "bafybeih2uiklkrin777kzhyk5khlnj35wapw2m5zeojrhglfosm76xjhym", + "custom/valory/trend_following_strategy/0.1.0": "bafybeibejqrhpxpcszjjq6sqqknk6ja72zfiyebqmpyw32e5hjd5hanx7e", + "custom/eightballer/always_buy/0.1.0": "bafybeic2fpf5ozhkf5jgzmppmfsprqw5ayfx6spgl3owuws464n7mkhpqi", "protocol/open_aea/signing/1.0.0": "bafybeihv62fim3wl2bayavfcg3u5e5cxu3b7brtu4cn5xoxd6lqwachasi", "protocol/valory/abci/0.1.0": "bafybeiaqmp7kocbfdboksayeqhkbrynvlfzsx4uy4x6nohywnmaig4an7u", "protocol/valory/contract_api/1.0.0": "bafybeidgu7o5llh26xp3u3ebq3yluull5lupiyeu6iooi2xyymdrgnzq5i", @@ -34,34 +26,40 @@ "protocol/valory/ledger_api/1.0.0": "bafybeihdk6psr4guxmbcrc26jr2cbgzpd5aljkqvpwo64bvaz7tdti2oni", "protocol/valory/acn/1.1.0": "bafybeidluaoeakae3exseupaea4i3yvvk5vivyt227xshjlffywwxzcxqe", "protocol/valory/ipfs/0.1.0": "bafybeiftxi2qhreewgsc5wevogi7yc5g6hbcbo4uiuaibauhv3nhfcdtvm", - "protocol/eightballer/order_book/0.1.0": "bafybeibtpf6fjlzfjfvwskveb7usb3bi27fbnzd5ypwm5u4oyzjnb3s6yi", - "protocol/eightballer/ohlcv/0.1.0": "bafybeibceyzlkap55isc7rcru3b3iosb2vhzz7xjt666k672bz6ejpsiyq", - "protocol/eightballer/balances/0.1.0": "bafybeiajh5vzhcofdpemm3545t3yh6g4okpwnejvbqchxapo765batiitu", - "protocol/eightballer/positions/0.1.0": "bafybeib6v2rtylru3lmri6tpgug7sgsd3imzqrpma3nuiqgjzmtdrsblaa", - "protocol/eightballer/spot_asset/0.1.0": "bafybeibrses5hkdzjtdbplkvvqfj7g64sopcdjwsstcyxujerttmpg4hxu", - "protocol/eightballer/orders/0.1.0": "bafybeibprhniaoq3y2uzc4arwwl7yws3i54ahaicrphh5gtl4xxhxqexdy", - "protocol/eightballer/tickers/0.1.0": "bafybeicjbpa24tla2enenmlzipqhu6grutqso74q6y7is2cpk7acub3bca", - "protocol/eightballer/default/0.1.0": "bafybeid2kyktyf2kqmua5dscnax7ustvht7krpwda6modxog5xrplwtmym", - "protocol/eightballer/markets/0.1.0": "bafybeibewtfadlw4kyknbjjxxjokrndea5mlrgsj7whmbfvhp5ksmnrsi4", + "protocol/eightballer/order_book/0.1.0": "bafybeigxj3gnjt4u5z5w2kksjy4stbbvg5kkbzgdxq5ikc35hum7l4eohe", + "protocol/eightballer/ohlcv/0.1.0": "bafybeie6qflg4oshhfm2us3btefmbruwtvr4qumgoca5glmrji7vqlgmzu", + "protocol/eightballer/balances/0.1.0": "bafybeicjxjwmr4wubghu4fa3cdg7dhwpy6b3dhvuaqbzem3jvycooyyazm", + "protocol/eightballer/positions/0.1.0": "bafybeiesp3psarxtmbjk625jpdemkduebnqhxmc6ikb66cr7wj23blphme", + "protocol/eightballer/spot_asset/0.1.0": "bafybeifjjvj2ds7grawmex4kilncabibqlrgvnuwcj3be2qxught37wixy", + "protocol/eightballer/orders/0.1.0": "bafybeifhwwwwj2ygxd3k5syf3oaohhhvwlb3frspszzofyatempnhnk3la", + "protocol/eightballer/tickers/0.1.0": "bafybeihmlyrsrztednef3p2nu42z5vnkur2jr46kbesxqujbfo3ap26jze", + "protocol/eightballer/default/0.1.0": "bafybeib7awcnlmjil7h7egx5fisncllmtgcva3odew3crz4ufdabhwess4", + "protocol/eightballer/markets/0.1.0": "bafybeifvgcpyqcxxptzqdd4xk23rqa5bjk3l7yggo6xo7dk5tivotmrdnm", "protocol/valory/tendermint/0.1.0": "bafybeig4mi3vmlv5zpbjbfuzcgida6j5f2nhrpedxicmrrfjweqc5r7cra", "contract/valory/service_registry/0.1.0": "bafybeihafe524ilngwzavkhwz4er56p7nyar26lfm7lrksfiqvvzo3kdcq", "contract/eightballer/erc_20/0.1.0": "bafybeiezbnm3f5zhuj5bsc542isnlh2fki5q4nmm2vsajzps4uuoamofo4", - "contract/eightballer/spl_token/0.1.0": "bafybeicoelddoxodp3k7v5gop2hva2xvxqtosueeej2rad6huy4byxkjni", + "contract/eightballer/spl_token/0.1.0": "bafybeidut74uztdcvhnw7qy6ui6e6ghsrg7hytehgeiysmcl7rkzkoyiaq", "contract/valory/gnosis_safe_proxy_factory/0.1.0": "bafybeicpcpyurm7gxir2gnlsgzeirzomkhcbnzr5txk67zdf4mmg737rtu", "contract/valory/multisend/0.1.0": "bafybeig5byt5urg2d2bsecufxe5ql7f4mezg3mekfleeh32nmuusx66p4y", "contract/valory/gnosis_safe/0.1.0": "bafybeib375xmvcplw7ageic2np3hq4yqeijrvd5kl7rrdnyvswats6ngmm", - "connection/eightballer/dcxt/0.1.0": "bafybeihes2id6qmqqybvdqfpchu6h3i5mpnhq6lztfxp52twd22iwqxacq", + "connection/eightballer/dcxt/0.1.0": "bafybeihnaxpiki57ivphf24qpie5g6rxwnztbrymutlw4yuyjxzvj3pdry", "connection/valory/abci/0.1.0": "bafybeiejymu4ul62zx6weoibnlsrfprfpjnplhjefz6sr6izgdr4sajlnu", "connection/valory/http_client/0.23.0": "bafybeihi772xgzpqeipp3fhmvpct4y6e6tpjp4sogwqrnf3wqspgeilg4u", "connection/valory/ipfs/0.1.0": "bafybeiegnapkvkamis47v5ioza2haerrjdzzb23rptpmcydyneas7jc2wm", "connection/valory/ledger/0.19.0": "bafybeigntoericenpzvwejqfuc3kqzo2pscs76qoygg5dbj6f4zxusru5e", "connection/valory/p2p_libp2p_client/0.1.0": "bafybeid3xg5k2ol5adflqloy75ibgljmol6xsvzvezebsg7oudxeeolz7e", "connection/valory/http_server/0.22.0": "bafybeihpgu56ovmq4npazdbh6y6ru5i7zuv6wvdglpxavsckyih56smu7m", + "skill/valory/trader_abci/0.1.0": "bafybeigco23fstumklkdfaatafca6eyv2vcwzabdsrzhlpmkyl7d3nbgv4", + "skill/valory/strategy_evaluator_abci/0.1.0": "bafybeibsk56r3dd5tza5yl2ltbonxdkb7zgfrupgiq3iyzr2ywqycskvz4", + "skill/valory/market_data_fetcher_abci/0.1.0": "bafybeibaf2ubj56busl7ngaamyjtm3tqsxthluuve6znxeogoxo7ta6b3y", + "skill/valory/portfolio_tracker_abci/0.1.0": "bafybeidprqeiomlvs6e5nvcqr2hnntwiuwyaxifidznjowoexx65yoplmq", + "skill/valory/trader_decision_maker_abci/0.1.0": "bafybeicskzcqr7dd4d5ulwss5u3hsx2wvcxatugtxiuja2neobbr3tqtkm", "skill/valory/abstract_abci/0.1.0": "bafybeidz54kvxhbdmpruzguuzzq7bjg4pekjb5amqobkxoy4oqknnobopu", "skill/valory/reset_pause_abci/0.1.0": "bafybeif4lgvbzsmzljesxbphycdv52ka7qnihyjrjpfaseclxadcmm6yiq", "skill/valory/registration_abci/0.1.0": "bafybeiffipsowrqrkhjoexem7ern5ob4fabgif7wa6gtlszcoaop2e3oey", "skill/valory/abstract_round_abci/0.1.0": "bafybeiajjzuh6vf23crp55humonknirvv2f4s3dmdlfzch6tc5ow52pcgm", "skill/valory/termination_abci/0.1.0": "bafybeiekkpo5qef5zaeagm3si6v45qxcojvtjqe4a5ceccvk4q7k3xi3bi", + "skill/valory/ipfs_package_downloader/0.1.0": "bafybeieokinjosnulrsee3kbj7ly4kjfx2ub6lmwkyplgs33vxgmx3fbvm", "skill/valory/transaction_settlement_abci/0.1.0": "bafybeielv6eivt2z6nforq43xewl2vmpfwpdu2s2vfogobziljnwsclmlm" } } \ No newline at end of file From 73c5f54c2b99c9691637512b6c06e6c2de865dac Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Thu, 24 Oct 2024 21:08:23 +0530 Subject: [PATCH 16/41] chore: Update pakacges --- packages/packages.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/packages.json b/packages/packages.json index c989985..7f0d783 100644 --- a/packages/packages.json +++ b/packages/packages.json @@ -10,8 +10,8 @@ "contract/valory/staking_activity_checker/0.1.0": "bafybeibjzsi2r5b6xd4iwl4wbwldptnynryzsdpifym4mkv32ynswx22ou", "skill/valory/liquidity_trader_abci/0.1.0": "bafybeihafrz72evtascc2q5f2thpvunmr7vec6gipvqp3upnhkvkbqqrnu", "skill/valory/optimus_abci/0.1.0": "bafybeiarvc5dcslcpjzatygne44jmhegvhzduzyf3gv65ih63zg77f4pp4", - "agent/valory/optimus/0.1.0": "bafybeicg2uhzgbybz64gdxm226mgg64htzxycnr7godrl7guk36da22cyy", - "service/valory/optimus/0.1.0": "bafybeihwqhbbepwhtgeaf2ysuf4dqdx6o3krj7tj74eh2jc2scwnffrooe" + "agent/valory/optimus/0.1.0": "bafybeifs5xrmjvbhadphsa5aoygabiorjzs263n65dw7w7epu6lhxav2ke", + "service/valory/optimus/0.1.0": "bafybeifvf6ntinfpkhmrv6ntxppv27xynoddx6ciejfp57vde7nak6ljpm" }, "third_party": { "custom/eightballer/rsi_strategy/0.1.0": "bafybeigbofp2nqwcxu3rlkuugpc3w6ils3u7glse7c335rddcqg56ybh34", @@ -19,6 +19,7 @@ "custom/eightballer/vwap_momentum/0.1.0": "bafybeih2uiklkrin777kzhyk5khlnj35wapw2m5zeojrhglfosm76xjhym", "custom/valory/trend_following_strategy/0.1.0": "bafybeibejqrhpxpcszjjq6sqqknk6ja72zfiyebqmpyw32e5hjd5hanx7e", "custom/eightballer/always_buy/0.1.0": "bafybeic2fpf5ozhkf5jgzmppmfsprqw5ayfx6spgl3owuws464n7mkhpqi", + "custom/eightballer/portfolio_balancer/0.1.0": "bafybeif6aje2tkvmakhhefqown654i3xr3l6erbbhntstllt7m37eenu5m", "protocol/open_aea/signing/1.0.0": "bafybeihv62fim3wl2bayavfcg3u5e5cxu3b7brtu4cn5xoxd6lqwachasi", "protocol/valory/abci/0.1.0": "bafybeiaqmp7kocbfdboksayeqhkbrynvlfzsx4uy4x6nohywnmaig4an7u", "protocol/valory/contract_api/1.0.0": "bafybeidgu7o5llh26xp3u3ebq3yluull5lupiyeu6iooi2xyymdrgnzq5i", From 483365578cf4fb1cb1266d219d32841a97901b49 Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Thu, 24 Oct 2024 21:08:49 +0530 Subject: [PATCH 17/41] chore: Update config files --- packages/valory/agents/optimus/aea-config.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/valory/agents/optimus/aea-config.yaml b/packages/valory/agents/optimus/aea-config.yaml index 7d1cd42..f6dcb3b 100644 --- a/packages/valory/agents/optimus/aea-config.yaml +++ b/packages/valory/agents/optimus/aea-config.yaml @@ -8,7 +8,7 @@ fingerprint: __init__.py: bafybeigx5mdvnamsqfum5ut7htok2y5vsnu7lrvms5gfvqi7hmv7sfbo3a fingerprint_ignore_patterns: [] connections: -- eightballer/dcxt:0.1.0:bafybeihes2id6qmqqybvdqfpchu6h3i5mpnhq6lztfxp52twd22iwqxacq +- eightballer/dcxt:0.1.0:bafybeihnaxpiki57ivphf24qpie5g6rxwnztbrymutlw4yuyjxzvj3pdry - valory/abci:0.1.0:bafybeiejymu4ul62zx6weoibnlsrfprfpjnplhjefz6sr6izgdr4sajlnu - valory/http_client:0.23.0:bafybeihi772xgzpqeipp3fhmvpct4y6e6tpjp4sogwqrnf3wqspgeilg4u - valory/http_server:0.22.0:bafybeihpgu56ovmq4npazdbh6y6ru5i7zuv6wvdglpxavsckyih56smu7m @@ -38,16 +38,16 @@ skills: - valory/abstract_abci:0.1.0:bafybeidz54kvxhbdmpruzguuzzq7bjg4pekjb5amqobkxoy4oqknnobopu - valory/abstract_round_abci:0.1.0:bafybeiajjzuh6vf23crp55humonknirvv2f4s3dmdlfzch6tc5ow52pcgm - valory/liquidity_trader_abci:0.1.0:bafybeihafrz72evtascc2q5f2thpvunmr7vec6gipvqp3upnhkvkbqqrnu -- valory/market_data_fetcher_abci:0.1.0:bafybeiffexwacktnhihmnhxdqs2msdzbigth62oqb7ghe2bqxwfkyvs5ty -- valory/strategy_evaluator_abci:0.1.0:bafybeicsxyuh2xa33h5jogq2bqumo3c3vutpsi5nj7aarfxfmy3jrjvxqe -- valory/optimus_abci:0.1.0:bafybeidtf4zg7qkef5biruh4vlcql2vd2ide4yhm4oza2vp263crx54wy4 +- valory/market_data_fetcher_abci:0.1.0:bafybeibaf2ubj56busl7ngaamyjtm3tqsxthluuve6znxeogoxo7ta6b3y +- valory/strategy_evaluator_abci:0.1.0:bafybeibsk56r3dd5tza5yl2ltbonxdkb7zgfrupgiq3iyzr2ywqycskvz4 +- valory/optimus_abci:0.1.0:bafybeiarvc5dcslcpjzatygne44jmhegvhzduzyf3gv65ih63zg77f4pp4 - valory/registration_abci:0.1.0:bafybeiffipsowrqrkhjoexem7ern5ob4fabgif7wa6gtlszcoaop2e3oey - valory/reset_pause_abci:0.1.0:bafybeif4lgvbzsmzljesxbphycdv52ka7qnihyjrjpfaseclxadcmm6yiq - valory/termination_abci:0.1.0:bafybeiekkpo5qef5zaeagm3si6v45qxcojvtjqe4a5ceccvk4q7k3xi3bi - valory/transaction_settlement_abci:0.1.0:bafybeielv6eivt2z6nforq43xewl2vmpfwpdu2s2vfogobziljnwsclmlm -- valory/trader_decision_maker_abci:0.1.0:bafybeigkks2o42hoemll7qxq7ulgidqynvjczmpa2ifuhnztltjf7p63b4 +- valory/trader_decision_maker_abci:0.1.0:bafybeicskzcqr7dd4d5ulwss5u3hsx2wvcxatugtxiuja2neobbr3tqtkm - valory/ipfs_package_downloader:0.1.0:bafybeieokinjosnulrsee3kbj7ly4kjfx2ub6lmwkyplgs33vxgmx3fbvm -- valory/portfolio_tracker_abci:0.1.0:bafybeiay2lasy2mtp5fxd77wgn2ocgy5neg42mlyitltuzrr7b6yzbduoa +- valory/portfolio_tracker_abci:0.1.0:bafybeidprqeiomlvs6e5nvcqr2hnntwiuwyaxifidznjowoexx65yoplmq default_ledger: ethereum required_ledgers: - ethereum @@ -101,7 +101,7 @@ config: poa_chain: ${bool:false} default_gas_price_strategy: ${str:eip1559} base: - address: ${str:https://virtual.base.rpc.tenderly.co/828d3af0-504a-47ed-bd0c-41fc71f15f74} + address: ${str:https://virtual.base.rpc.tenderly.co/70dfa32a-6f92-4dd8-ba44-45bc16760de9} chain_id: ${int:8453} poa_chain: ${bool:false} default_gas_price_strategy: ${str:eip1559} @@ -344,5 +344,5 @@ config: - name: ${str:balancer} key_path: ${str:ethereum_private_key.txt} ledger_id: ${str:base} - rpc_url: ${str:https://base.blockpi.network/v1/rpc/public} + rpc_url: ${str:https://virtual.base.rpc.tenderly.co/70dfa32a-6f92-4dd8-ba44-45bc16760de9} etherscan_api_key: ${str:YOUR_ETHERSCAN_API_KEY} From 333f99fe188143c9bf97c4144fa4d76aaea085e4 Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Fri, 25 Oct 2024 18:21:16 +0530 Subject: [PATCH 18/41] chore: added round changes --- .../valory/agents/optimus/aea-config.yaml | 9 +++-- .../liquidity_trader_abci/behaviours.py | 36 ++++++++++++++++- .../skills/liquidity_trader_abci/payloads.py | 5 +++ .../skills/liquidity_trader_abci/rounds.py | 40 ++++++++++++++++++- .../skills/liquidity_trader_abci/skill.yaml | 6 +-- .../valory/skills/optimus_abci/composition.py | 3 +- 6 files changed, 89 insertions(+), 10 deletions(-) diff --git a/packages/valory/agents/optimus/aea-config.yaml b/packages/valory/agents/optimus/aea-config.yaml index f6dcb3b..9fa2a61 100644 --- a/packages/valory/agents/optimus/aea-config.yaml +++ b/packages/valory/agents/optimus/aea-config.yaml @@ -37,10 +37,10 @@ protocols: skills: - valory/abstract_abci:0.1.0:bafybeidz54kvxhbdmpruzguuzzq7bjg4pekjb5amqobkxoy4oqknnobopu - valory/abstract_round_abci:0.1.0:bafybeiajjzuh6vf23crp55humonknirvv2f4s3dmdlfzch6tc5ow52pcgm -- valory/liquidity_trader_abci:0.1.0:bafybeihafrz72evtascc2q5f2thpvunmr7vec6gipvqp3upnhkvkbqqrnu +- valory/liquidity_trader_abci:0.1.0:bafybeiaseqi3uzajwqboejbgndxbfbvgjcn7a6vvncmfxjegpvam3m3e3y - valory/market_data_fetcher_abci:0.1.0:bafybeibaf2ubj56busl7ngaamyjtm3tqsxthluuve6znxeogoxo7ta6b3y - valory/strategy_evaluator_abci:0.1.0:bafybeibsk56r3dd5tza5yl2ltbonxdkb7zgfrupgiq3iyzr2ywqycskvz4 -- valory/optimus_abci:0.1.0:bafybeiarvc5dcslcpjzatygne44jmhegvhzduzyf3gv65ih63zg77f4pp4 +- valory/optimus_abci:0.1.0:bafybeicsgjtgrfqgfhqnecm55d2hxrsqjox5w422adqsabvg2pocb5fvcu - valory/registration_abci:0.1.0:bafybeiffipsowrqrkhjoexem7ern5ob4fabgif7wa6gtlszcoaop2e3oey - valory/reset_pause_abci:0.1.0:bafybeif4lgvbzsmzljesxbphycdv52ka7qnihyjrjpfaseclxadcmm6yiq - valory/termination_abci:0.1.0:bafybeiekkpo5qef5zaeagm3si6v45qxcojvtjqe4a5ceccvk4q7k3xi3bi @@ -105,6 +105,9 @@ config: chain_id: ${int:8453} poa_chain: ${bool:false} default_gas_price_strategy: ${str:eip1559} + gas_price_strategies: + eip1559: + default_priority_fee: 500 optimism: address: ${str:https://virtual.optimism.rpc.tenderly.co/3baf4a62-2fa9-448a-91a6-5f6ab95c76be} chain_id: ${int:10} @@ -265,7 +268,7 @@ models: serious_slash_unit_amount: ${int:8000000000000000} multisend_batch_size: ${int:50} ipfs_address: ${str:https://gateway.autonolas.tech/ipfs/} - default_chain_id: ${str:optimism} + default_chain_id: ${str:base} termination_from_block: ${int:34088325} allowed_dexs: ${list:["balancerPool", "UniswapV3"]} initial_assets: ${str:{"ethereum":{"0x0000000000000000000000000000000000000000":"ETH","0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48":"USDC"}}} diff --git a/packages/valory/skills/liquidity_trader_abci/behaviours.py b/packages/valory/skills/liquidity_trader_abci/behaviours.py index d8ee1f4..4f502d9 100644 --- a/packages/valory/skills/liquidity_trader_abci/behaviours.py +++ b/packages/valory/skills/liquidity_trader_abci/behaviours.py @@ -92,7 +92,9 @@ StakingState, SynchronizedData, DecideAgentStartingRound, - DecideAgentStartingPayload + DecideAgentStartingPayload, + DecideAgentEndingRound, + DecideAgentEndingPayload, ) from packages.valory.skills.liquidity_trader_abci.strategies.simple_strategy import ( SimpleStrategyBehaviour, @@ -3383,7 +3385,36 @@ def async_act(self) -> Generator: yield from self.send_a2a_transaction(payload) yield from self.wait_until_round_end() - self.set_done() + self.set_done() + +class DecideAgentEndingBehaviour(LiquidityTraderBaseBehaviour): + """Behaviour that executes all the actions.""" + + matching_round: Type[AbstractRound] = DecideAgentEndingRound + + def async_act(self) -> Generator: + """Async act""" + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + sender = self.context.agent_address + if self.params.agent_transition is True: + next_event = Event.MOVE_TO_NEXT_AGENT.value + else: + next_event = Event.DONT_MOVE_TO_NEXT_AGENT.value + + payload = DecideAgentEndingPayload( + sender=sender, + content=json.dumps( + { + "event": next_event + } + ), + ) + + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + yield from self.send_a2a_transaction(payload) + yield from self.wait_until_round_end() + + self.set_done() class LiquidityTraderRoundBehaviour(AbstractRoundBehaviour): """LiquidityTraderRoundBehaviour""" @@ -3397,6 +3428,7 @@ class LiquidityTraderRoundBehaviour(AbstractRoundBehaviour): EvaluateStrategyBehaviour, DecisionMakingBehaviour, DecideAgentStartingBehaviour, + DecideAgentEndingBehaviour, PostTxSettlementBehaviour, ] diff --git a/packages/valory/skills/liquidity_trader_abci/payloads.py b/packages/valory/skills/liquidity_trader_abci/payloads.py index 5e6d2c8..f3a4d61 100644 --- a/packages/valory/skills/liquidity_trader_abci/payloads.py +++ b/packages/valory/skills/liquidity_trader_abci/payloads.py @@ -76,6 +76,11 @@ class DecideAgentStartingPayload(BaseTxPayload): content: str +@dataclass(frozen=True) +class DecideAgentEndingPayload(BaseTxPayload): + """Represent a transaction payload for the DecideAgentRound.""" + + content: str @dataclass(frozen=True) class PostTxSettlementPayload(BaseTxPayload): diff --git a/packages/valory/skills/liquidity_trader_abci/rounds.py b/packages/valory/skills/liquidity_trader_abci/rounds.py index 3ea6ac8..a908ab9 100644 --- a/packages/valory/skills/liquidity_trader_abci/rounds.py +++ b/packages/valory/skills/liquidity_trader_abci/rounds.py @@ -41,7 +41,8 @@ EvaluateStrategyPayload, GetPositionsPayload, PostTxSettlementPayload, - DecideAgentStartingPayload + DecideAgentStartingPayload, + DecideAgentEndingPayload, ) @@ -420,6 +421,29 @@ def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: return self.synchronized_data, Event.NO_MAJORITY return None +class DecideAgentEndingRound(CollectSameUntilThresholdRound): + """DecideAgentRound""" + + payload_class = DecideAgentEndingPayload + synchronized_data_class = SynchronizedData + done_event = Event.DONE + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: + """Process the end of the block.""" + if self.threshold_reached: + # We reference all the events here to prevent the check-abciapp-specs tool from complaining + payload = json.loads(self.most_voted_payload) + event = Event(payload["event"]) + synchronized_data = cast(SynchronizedData, self.synchronized_data) + + return synchronized_data, event + + if not self.is_majority_possible( + self.collection, self.synchronized_data.nb_participants + ): + return self.synchronized_data, Event.NO_MAJORITY + return None + class PostTxSettlementRound(CollectSameUntilThresholdRound): """A round that will be called after tx settlement is done.""" @@ -473,6 +497,9 @@ class FailedMultiplexerRound(DegenerateRound): class SwitchAgentStartingRound(DegenerateRound): """SwitchAgentRound""" +class SwitchAgentEndingRound(DegenerateRound): + """SwitchAgentRound""" + class LiquidityTraderAbciApp(AbciApp[Event]): """LiquidityTraderAbciApp""" @@ -480,6 +507,7 @@ class LiquidityTraderAbciApp(AbciApp[Event]): initial_states: Set[AppState] = { CallCheckpointRound, DecideAgentStartingRound, + DecideAgentEndingRound, CheckStakingKPIMetRound, GetPositionsRound, DecisionMakingRound, @@ -529,6 +557,12 @@ class LiquidityTraderAbciApp(AbciApp[Event]): Event.DONE: CallCheckpointRound, Event.NO_MAJORITY: CallCheckpointRound, }, + DecideAgentEndingRound: { + Event.DONT_MOVE_TO_NEXT_AGENT: PostTxSettlementRound, + Event.MOVE_TO_NEXT_AGENT: SwitchAgentEndingRound, + Event.DONE: PostTxSettlementRound, + Event.NO_MAJORITY: PostTxSettlementRound, + }, PostTxSettlementRound: { Event.ACTION_EXECUTED: DecisionMakingRound, Event.CHECKPOINT_TX_EXECUTED: CallCheckpointRound, @@ -540,6 +574,7 @@ class LiquidityTraderAbciApp(AbciApp[Event]): FinishedTxPreparationRound: {}, FinishedDecisionMakingRound: {}, SwitchAgentStartingRound:{}, + SwitchAgentEndingRound:{}, FinishedCallCheckpointRound: {}, FinishedCheckStakingKPIMetRound: {}, FailedMultiplexerRound: {}, @@ -548,6 +583,7 @@ class LiquidityTraderAbciApp(AbciApp[Event]): FinishedEvaluateStrategyRound, FinishedDecisionMakingRound, SwitchAgentStartingRound, + SwitchAgentEndingRound, FinishedTxPreparationRound, FinishedCallCheckpointRound, FinishedCheckStakingKPIMetRound, @@ -569,6 +605,7 @@ class LiquidityTraderAbciApp(AbciApp[Event]): CheckStakingKPIMetRound: set(), GetPositionsRound: set(), DecideAgentStartingRound: set(), + DecideAgentEndingRound: set(), DecisionMakingRound: set(), PostTxSettlementRound: set(), } @@ -581,5 +618,6 @@ class LiquidityTraderAbciApp(AbciApp[Event]): FinishedEvaluateStrategyRound: set(), FinishedDecisionMakingRound: set(), SwitchAgentStartingRound: set(), + SwitchAgentEndingRound: set(), FinishedTxPreparationRound: {get_name(SynchronizedData.most_voted_tx_hash)}, } diff --git a/packages/valory/skills/liquidity_trader_abci/skill.yaml b/packages/valory/skills/liquidity_trader_abci/skill.yaml index 7b91305..ebd7db7 100644 --- a/packages/valory/skills/liquidity_trader_abci/skill.yaml +++ b/packages/valory/skills/liquidity_trader_abci/skill.yaml @@ -7,16 +7,16 @@ license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeia7bn2ahqqwkf63ptje6rfnftuwrsp33sswgpcbh5osbesxxr6g4m - behaviours.py: bafybeigkt4ay2bydfntmtzadl5xtz3pdk2nvityudwyplc3n6xswhuhjwy + behaviours.py: bafybeibkyfgwxu5bqw33vny3loat3liqe74emq6d5uxxnwzobx3kvydaca dialogues.py: bafybeiay23otskx2go5xhtgdwfw2kd6rxd62sxxdu3njv7hageorl5zxzm fsm_specification.yaml: bafybeiabbiulb7k6xkjysulmy6o4ugnhxlpp5jiaeextvwj65q4ttadoeq handlers.py: bafybeidxw2lvgiifmo4siobpwuwbxscuifrdo3gnkjyn6bgexotj5f7zf4 models.py: bafybeihhdgk5ui4qmcszamyaxthtgizrlmmbmlo2y6hu7taqis5u44favq - payloads.py: bafybeifl4vmex2tj4k4ysv65ds5kzdjpydusxoatph7avfsbebfuhnygvm + payloads.py: bafybeidrarae35mhurj3d6hgeznqpa2a522x42rjmcfbhnrhyoe2rayxgy pool_behaviour.py: bafybeiaheuesscgqzwjbpyrezgwpdbdfurlmfwbc462qv6rblwwxlx5dpm pools/balancer.py: bafybeigznhgv7ylo5dvlhxcqikhiuqlqtnx3ikv4tszyvkl2lpcuqgoa5u pools/uniswap.py: bafybeigmqptgmjaxscszohfusgxsexqyx4awuyw7p4g5l7k2qpeyq7vdcu - rounds.py: bafybeidm3pkkrokqpyb7ncjfvekaezpx57o2mmyc2qgcn7wagtmu5xsyqm + rounds.py: bafybeiajgtt32rl32mgtd6t6qjxrx4xcsemfmsryemrdadzvow4cyjgcsy strategies/simple_strategy.py: bafybeiasu2nchowx6leksjllpuum4ckezxoj4o2m4sstavblplvvutmvzm strategy_behaviour.py: bafybeidk6sorg47kuuubamcccksi65x3txldyo7y2hm5opbye2ghmz2ljy fingerprint_ignore_patterns: [] diff --git a/packages/valory/skills/optimus_abci/composition.py b/packages/valory/skills/optimus_abci/composition.py index 26e66c6..c68818f 100644 --- a/packages/valory/skills/optimus_abci/composition.py +++ b/packages/valory/skills/optimus_abci/composition.py @@ -61,7 +61,8 @@ LiquidityTraderAbci.FinishedEvaluateStrategyRound: ResetAndPauseAbci.ResetAndPauseRound, LiquidityTraderAbci.FinishedTxPreparationRound: TransactionSettlementAbci.RandomnessTransactionSubmissionRound, LiquidityTraderAbci.FailedMultiplexerRound: ResetAndPauseAbci.ResetAndPauseRound, - TransactionSettlementAbci.FinishedTransactionSubmissionRound: LiquidityTraderAbci.PostTxSettlementRound, + TransactionSettlementAbci.FinishedTransactionSubmissionRound: LiquidityTraderAbci.DecideAgentEndingRound, + LiquidityTraderAbci.SwitchAgentEndingRound:TraderDecisionMakerAbci.RandomnessRound, TransactionSettlementAbci.FailedRound: ResetAndPauseAbci.ResetAndPauseRound, ResetAndPauseAbci.FinishedResetAndPauseRound: LiquidityTraderAbci.DecideAgentStartingRound, ResetAndPauseAbci.FinishedResetAndPauseErrorRound: RegistrationAbci.RegistrationRound, From 6046f455e0c38e2d463eee0bf280855c9b700d9b Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Sat, 26 Oct 2024 05:15:04 +0530 Subject: [PATCH 19/41] chore: Update to Voting Round --- .../valory/agents/optimus/aea-config.yaml | 4 +- .../liquidity_trader_abci/behaviours.py | 34 ++----------- .../skills/liquidity_trader_abci/payloads.py | 4 +- .../skills/liquidity_trader_abci/rounds.py | 48 ++++++++++++------- 4 files changed, 40 insertions(+), 50 deletions(-) diff --git a/packages/valory/agents/optimus/aea-config.yaml b/packages/valory/agents/optimus/aea-config.yaml index 9fa2a61..8e25de1 100644 --- a/packages/valory/agents/optimus/aea-config.yaml +++ b/packages/valory/agents/optimus/aea-config.yaml @@ -37,10 +37,10 @@ protocols: skills: - valory/abstract_abci:0.1.0:bafybeidz54kvxhbdmpruzguuzzq7bjg4pekjb5amqobkxoy4oqknnobopu - valory/abstract_round_abci:0.1.0:bafybeiajjzuh6vf23crp55humonknirvv2f4s3dmdlfzch6tc5ow52pcgm -- valory/liquidity_trader_abci:0.1.0:bafybeiaseqi3uzajwqboejbgndxbfbvgjcn7a6vvncmfxjegpvam3m3e3y +- valory/liquidity_trader_abci:0.1.0:bafybeib2r7z5ufnb5nmtvob4bejb6ql3rrocqb6oinsze7xcaa33ulibye - valory/market_data_fetcher_abci:0.1.0:bafybeibaf2ubj56busl7ngaamyjtm3tqsxthluuve6znxeogoxo7ta6b3y - valory/strategy_evaluator_abci:0.1.0:bafybeibsk56r3dd5tza5yl2ltbonxdkb7zgfrupgiq3iyzr2ywqycskvz4 -- valory/optimus_abci:0.1.0:bafybeicsgjtgrfqgfhqnecm55d2hxrsqjox5w422adqsabvg2pocb5fvcu +- valory/optimus_abci:0.1.0:bafybeider6qgvmw64zzigonfp6coaap3gubredzqcz7oq7luf3aqkqincm - valory/registration_abci:0.1.0:bafybeiffipsowrqrkhjoexem7ern5ob4fabgif7wa6gtlszcoaop2e3oey - valory/reset_pause_abci:0.1.0:bafybeif4lgvbzsmzljesxbphycdv52ka7qnihyjrjpfaseclxadcmm6yiq - valory/termination_abci:0.1.0:bafybeiekkpo5qef5zaeagm3si6v45qxcojvtjqe4a5ceccvk4q7k3xi3bi diff --git a/packages/valory/skills/liquidity_trader_abci/behaviours.py b/packages/valory/skills/liquidity_trader_abci/behaviours.py index 4f502d9..5f632e9 100644 --- a/packages/valory/skills/liquidity_trader_abci/behaviours.py +++ b/packages/valory/skills/liquidity_trader_abci/behaviours.py @@ -3366,20 +3366,7 @@ class DecideAgentStartingBehaviour(LiquidityTraderBaseBehaviour): def async_act(self) -> Generator: """Async act""" with self.context.benchmark_tool.measure(self.behaviour_id).local(): - sender = self.context.agent_address - if self.params.agent_transition is True: - next_event = Event.MOVE_TO_NEXT_AGENT.value - else: - next_event = Event.DONT_MOVE_TO_NEXT_AGENT.value - - payload = DecideAgentStartingPayload( - sender=sender, - content=json.dumps( - { - "event": next_event - } - ), - ) + payload = DecideAgentStartingPayload(self.context.agent_address, self.params.agent_transition) with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): yield from self.send_a2a_transaction(payload) @@ -3395,26 +3382,13 @@ class DecideAgentEndingBehaviour(LiquidityTraderBaseBehaviour): def async_act(self) -> Generator: """Async act""" with self.context.benchmark_tool.measure(self.behaviour_id).local(): - sender = self.context.agent_address - if self.params.agent_transition is True: - next_event = Event.MOVE_TO_NEXT_AGENT.value - else: - next_event = Event.DONT_MOVE_TO_NEXT_AGENT.value - - payload = DecideAgentEndingPayload( - sender=sender, - content=json.dumps( - { - "event": next_event - } - ), - ) - + payload = DecideAgentEndingPayload(self.context.agent_address, self.params.agent_transition) + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): yield from self.send_a2a_transaction(payload) yield from self.wait_until_round_end() - self.set_done() + self.set_done() class LiquidityTraderRoundBehaviour(AbstractRoundBehaviour): """LiquidityTraderRoundBehaviour""" diff --git a/packages/valory/skills/liquidity_trader_abci/payloads.py b/packages/valory/skills/liquidity_trader_abci/payloads.py index f3a4d61..3bf6811 100644 --- a/packages/valory/skills/liquidity_trader_abci/payloads.py +++ b/packages/valory/skills/liquidity_trader_abci/payloads.py @@ -74,13 +74,13 @@ class DecisionMakingPayload(BaseTxPayload): class DecideAgentStartingPayload(BaseTxPayload): """Represent a transaction payload for the DecideAgentRound.""" - content: str + vote: bool @dataclass(frozen=True) class DecideAgentEndingPayload(BaseTxPayload): """Represent a transaction payload for the DecideAgentRound.""" - content: str + vote: bool @dataclass(frozen=True) class PostTxSettlementPayload(BaseTxPayload): diff --git a/packages/valory/skills/liquidity_trader_abci/rounds.py b/packages/valory/skills/liquidity_trader_abci/rounds.py index a908ab9..3d228e1 100644 --- a/packages/valory/skills/liquidity_trader_abci/rounds.py +++ b/packages/valory/skills/liquidity_trader_abci/rounds.py @@ -33,6 +33,7 @@ DegenerateRound, DeserializedCollection, get_name, + VotingRound, ) from packages.valory.skills.liquidity_trader_abci.payloads import ( CallCheckpointPayload, @@ -56,7 +57,8 @@ class StakingState(Enum): class Event(Enum): """LiquidityTraderAbciApp Events""" - + NEGATIVE = "negative" + NONE = "none" ACTION_EXECUTED = "execute_next_action" CHECKPOINT_TX_EXECUTED = "checkpoint_tx_executed" DONE = "done" @@ -398,51 +400,64 @@ def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: return self.synchronized_data, Event.NO_MAJORITY return None -class DecideAgentStartingRound(CollectSameUntilThresholdRound): +class DecideAgentStartingRound(VotingRound): """DecisionMakingRound""" payload_class = DecideAgentStartingPayload synchronized_data_class = SynchronizedData done_event = Event.DONE + negative_event = Event.NEGATIVE + none_event = Event.NONE + no_majority_event = Event.NO_MAJORITY + collection_key = get_name(SynchronizedData.participant_to_votes) def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: """Process the end of the block.""" - if self.threshold_reached: + if self.positive_vote_threshold_reached: # We reference all the events here to prevent the check-abciapp-specs tool from complaining - payload = json.loads(self.most_voted_payload) - event = Event(payload["event"]) synchronized_data = cast(SynchronizedData, self.synchronized_data) - return synchronized_data, event + return synchronized_data, Event.MOVE_TO_NEXT_AGENT + + if self.negative_vote_threshold_reached: + return self.synchronized_data, Event.DONT_MOVE_TO_NEXT_AGENT + if self.none_vote_threshold_reached: + return self.synchronized_data, self.none_event if not self.is_majority_possible( self.collection, self.synchronized_data.nb_participants ): - return self.synchronized_data, Event.NO_MAJORITY + return self.synchronized_data, self.no_majority_event return None + -class DecideAgentEndingRound(CollectSameUntilThresholdRound): +class DecideAgentEndingRound(VotingRound): """DecideAgentRound""" payload_class = DecideAgentEndingPayload synchronized_data_class = SynchronizedData done_event = Event.DONE + negative_event = Event.NEGATIVE + none_event = Event.NONE + no_majority_event = Event.NO_MAJORITY + collection_key = get_name(SynchronizedData.participant_to_votes) def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: """Process the end of the block.""" - if self.threshold_reached: + if self.positive_vote_threshold_reached: # We reference all the events here to prevent the check-abciapp-specs tool from complaining - payload = json.loads(self.most_voted_payload) - event = Event(payload["event"]) synchronized_data = cast(SynchronizedData, self.synchronized_data) - - return synchronized_data, event - + return synchronized_data, Event.MOVE_TO_NEXT_AGENT + + if self.negative_vote_threshold_reached: + return self.synchronized_data, Event.DONT_MOVE_TO_NEXT_AGENT + if self.none_vote_threshold_reached: + return self.synchronized_data, self.none_event if not self.is_majority_possible( self.collection, self.synchronized_data.nb_participants ): - return self.synchronized_data, Event.NO_MAJORITY - return None + return self.synchronized_data, self.no_majority_event + return None class PostTxSettlementRound(CollectSameUntilThresholdRound): """A round that will be called after tx settlement is done.""" @@ -555,6 +570,7 @@ class LiquidityTraderAbciApp(AbciApp[Event]): Event.DONT_MOVE_TO_NEXT_AGENT: CallCheckpointRound, Event.MOVE_TO_NEXT_AGENT: SwitchAgentStartingRound, Event.DONE: CallCheckpointRound, + Event.NONE: CallCheckpointRound, Event.NO_MAJORITY: CallCheckpointRound, }, DecideAgentEndingRound: { From 977aa52f1aff2a50072efc07bbd1f53109535509 Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Mon, 28 Oct 2024 11:27:04 +0530 Subject: [PATCH 20/41] chore: update skills yaml in optimus --- .../valory/skills/optimus_abci/skill.yaml | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/valory/skills/optimus_abci/skill.yaml b/packages/valory/skills/optimus_abci/skill.yaml index 0f7ec79..c39ca31 100644 --- a/packages/valory/skills/optimus_abci/skill.yaml +++ b/packages/valory/skills/optimus_abci/skill.yaml @@ -8,29 +8,29 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeiechr3zr5bc4xl3vvs2p2gti54ily7ao2gu4ff5lys6cixehzkdea behaviours.py: bafybeidutyxryf6h7ooc7ggrdl5lojfv3anrvf3zq4tbwkiwxm7qqrriye - composition.py: bafybeiff7fikz76hrhfv6cgajfov3kb2ivkhkzmvhqzfnchqjtdxh6qb2u + composition.py: bafybeiczokwfi2h6qkw4lk5cp52tafddxxysh6qwrnzqt4cfu67yb5a2nq dialogues.py: bafybeift7d4mk4qkzm4rqmdrfv43erhunzyqmwn6pecuumq25amy7q7fp4 fsm_specification.yaml: bafybeiehe6ps7xi7i7tu4mduid7vbvszhsiu5juyard24i3nhtqgljpcza - handlers.py: bafybeife4nrwqiwrx2ucza3vk6t5inpkncuewehtdnitax4lmqq2ptoona - models.py: bafybeihwcvw363vsglhgtneklcihcngnqmegjzasjow3ftxkfwnopomhoe + handlers.py: bafybeibjxph27kmpljyeqkednkrfgrtelksfs3kbgjbvh6eycjrfqz5o2y + models.py: bafybeibbnyxant6quqcbzwebu5jyws46mjpvpvsxnr3yrnq6slhrbd6zsa fingerprint_ignore_patterns: [] connections: [] contracts: [] protocols: -- eightballer/tickers:0.1.0:bafybeicjbpa24tla2enenmlzipqhu6grutqso74q6y7is2cpk7acub3bca -- eightballer/orders:0.1.0:bafybeibprhniaoq3y2uzc4arwwl7yws3i54ahaicrphh5gtl4xxhxqexdy -- eightballer/balances:0.1.0:bafybeiajh5vzhcofdpemm3545t3yh6g4okpwnejvbqchxapo765batiitu +- eightballer/tickers:0.1.0:bafybeihmlyrsrztednef3p2nu42z5vnkur2jr46kbesxqujbfo3ap26jze +- eightballer/orders:0.1.0:bafybeifhwwwwj2ygxd3k5syf3oaohhhvwlb3frspszzofyatempnhnk3la +- eightballer/balances:0.1.0:bafybeicjxjwmr4wubghu4fa3cdg7dhwpy6b3dhvuaqbzem3jvycooyyazm skills: - valory/abstract_round_abci:0.1.0:bafybeiajjzuh6vf23crp55humonknirvv2f4s3dmdlfzch6tc5ow52pcgm - valory/registration_abci:0.1.0:bafybeiffipsowrqrkhjoexem7ern5ob4fabgif7wa6gtlszcoaop2e3oey - valory/reset_pause_abci:0.1.0:bafybeif4lgvbzsmzljesxbphycdv52ka7qnihyjrjpfaseclxadcmm6yiq - valory/termination_abci:0.1.0:bafybeiekkpo5qef5zaeagm3si6v45qxcojvtjqe4a5ceccvk4q7k3xi3bi -- valory/liquidity_trader_abci:0.1.0:bafybeidccg5fwmxcdoo6llhxbyqwtxjayvz7byd5c4h6q7fsqkma4uioru +- valory/liquidity_trader_abci:0.1.0:bafybeib2r7z5ufnb5nmtvob4bejb6ql3rrocqb6oinsze7xcaa33ulibye - valory/transaction_settlement_abci:0.1.0:bafybeielv6eivt2z6nforq43xewl2vmpfwpdu2s2vfogobziljnwsclmlm -- valory/market_data_fetcher_abci:0.1.0:bafybeiffexwacktnhihmnhxdqs2msdzbigth62oqb7ghe2bqxwfkyvs5ty +- valory/market_data_fetcher_abci:0.1.0:bafybeibaf2ubj56busl7ngaamyjtm3tqsxthluuve6znxeogoxo7ta6b3y - valory/trader_decision_maker_abci:0.1.0:bafybeicskzcqr7dd4d5ulwss5u3hsx2wvcxatugtxiuja2neobbr3tqtkm -- valory/strategy_evaluator_abci:0.1.0:bafybeibdq4s53qakp55lagj6i2aut5vvqzidcvmo6bnqezie6ki7uzqnta -- valory/portfolio_tracker_abci:0.1.0:bafybeiay2lasy2mtp5fxd77wgn2ocgy5neg42mlyitltuzrr7b6yzbduoa +- valory/strategy_evaluator_abci:0.1.0:bafybeibsk56r3dd5tza5yl2ltbonxdkb7zgfrupgiq3iyzr2ywqycskvz4 +- valory/portfolio_tracker_abci:0.1.0:bafybeidprqeiomlvs6e5nvcqr2hnntwiuwyaxifidznjowoexx65yoplmq behaviours: main: args: {} @@ -57,6 +57,15 @@ handlers: tendermint: args: {} class_name: TendermintHandler + tickers: + args: {} + class_name: DcxtTickersHandler + balances: + args: {} + class_name: DcxtBalancesHandler + orders: + args: {} + class_name: DcxtOrdersHandler models: abci_dialogues: args: {} @@ -183,6 +192,7 @@ models: min_swap_amount_threshold: 10 max_fee_percentage: 0.02 max_gas_percentage: 0.1 + proxy_round_timeout_seconds: 1200.0 agent_transition: false merkl_fetch_campaigns_args: '{"url":"https://api.merkl.xyz/v3/campaigns","creator":"","live":"true"}' balancer_graphql_endpoints: '{"optimism":"https://api.studio.thegraph.com/query/75376/balancer-optimism-v2/version/latest","base":"https://api.studio.thegraph.com/query/24660/balancer-base-v2/version/latest"}' From dc86dad2e3dce21be4345c0b588d0dd9953d089a Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Mon, 28 Oct 2024 14:31:26 +0530 Subject: [PATCH 21/41] chore: Add third-party submodules for balpy and multicaller --- .gitmodules | 3 ++- third_party/balpy | 1 + third_party/multicaller | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) create mode 160000 third_party/balpy create mode 160000 third_party/multicaller diff --git a/.gitmodules b/.gitmodules index ee2ecca..0356e2b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,8 @@ + + [submodule "third_party/balpy"] path = third_party/balpy url = https://github.com/8ball030/balpy.git [submodule "third_party/multicaller"] path = third_party/multicaller url = https://github.com/8ball030/multicaller.git - diff --git a/third_party/balpy b/third_party/balpy new file mode 160000 index 0000000..4269105 --- /dev/null +++ b/third_party/balpy @@ -0,0 +1 @@ +Subproject commit 42691053c56f7740e2b1eb7981c3db99907ae362 diff --git a/third_party/multicaller b/third_party/multicaller new file mode 160000 index 0000000..0d4953d --- /dev/null +++ b/third_party/multicaller @@ -0,0 +1 @@ +Subproject commit 0d4953d527cd2282b30646881fa02ec5f5e9a678 From 31ec2131c837e913b3efda58526917356d1a6b59 Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Mon, 28 Oct 2024 14:32:05 +0530 Subject: [PATCH 22/41] chore: Update .gitignore --- .gitignore | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index 2c06345..3c8481d 100644 --- a/.gitignore +++ b/.gitignore @@ -46,14 +46,15 @@ packages/valory/skills/registration_abci/ packages/valory/skills/reset_pause_abci/ packages/valory/skills/transaction_settlement_abci/ packages/valory/skills/termination_abci/ +packages/valory/skills/ipfs_package_downloader/ +packages/valory/skills/market_data_fetcher_abci/ +packages/valory/skills/portfolio_tracker_abci/ +packages/valory/skills/strategy_evaluator_abci/ +packages/valory/skills/trader_decision_maker_abci +packages/valory/customs -packages/valory/protocols/abci -packages/valory/protocols/acn -packages/valory/protocols/contract_api -packages/valory/protocols/http -packages/valory/protocols/ipfs -packages/valory/protocols/ledger_api -packages/valory/protocols/tendermint + +packages/valory/protocols/* .idea **/__pycache__/ @@ -80,15 +81,4 @@ node_modules/ .env packages/open_aea/protocols/signing -packages/eightballer/protocols/order_book -packages/eightballer/protocols/ohlcv -packages/eightballer/protocols/balances -packages/eightballer/protocols/positions -packages/eightballer/protocols/spot_asset -packages/eightballer/protocols/orders -packages/eightballer/protocols/tickers -packages/eightballer/protocols/default -packages/eightballer/protocols/markets -packages/eightballer/contracts/erc_20 -packages/eightballer/contracts/spl_token -packages/eightballer/connections/dcxt +packages/eightballer/* From 688ada82241fb19eba101bfa4cb9868596232e1f Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Mon, 28 Oct 2024 14:45:29 +0530 Subject: [PATCH 23/41] chore: Update paths in agent config --- packages/valory/agents/optimus/aea-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/valory/agents/optimus/aea-config.yaml b/packages/valory/agents/optimus/aea-config.yaml index 8e25de1..7aaaf68 100644 --- a/packages/valory/agents/optimus/aea-config.yaml +++ b/packages/valory/agents/optimus/aea-config.yaml @@ -152,7 +152,7 @@ type: skill models: benchmark_tool: args: - log_dir: ${str:/Users/gauravlochab/repos/optimus/logs} + log_dir: ${str:/logs} get_balance: args: api_id: ${str:get_balance} @@ -303,7 +303,7 @@ models: staking_token_contract_address: ${str:0x88996bbdE7f982D93214881756840cE2c77C4992} staking_activity_checker_contract_address: ${str:0x7Fd1F4b764fA41d19fe3f63C85d12bf64d2bbf68} staking_threshold_period: ${int:5} - store_path: ${str:/Users/gauravlochab/repos/optimus/data/} + store_path: ${str:/data/} assets_info_filename: ${str:assets.json} pool_info_filename: ${str:current_pool.json} gas_cost_info_filename: ${str:gas_costs.json} From e8d21f859f1458a25c517a593715443a11b6cc75 Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Tue, 29 Oct 2024 12:39:34 +0530 Subject: [PATCH 24/41] chore: Update balpy and open-multicaller dependencies --- packages/packages.json | 16 ++++++++-------- pyproject.toml | 4 ++-- tox.ini | 2 ++ 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/packages.json b/packages/packages.json index faee822..4d84e82 100644 --- a/packages/packages.json +++ b/packages/packages.json @@ -8,10 +8,10 @@ "contract/valory/merkl_distributor/0.1.0": "bafybeihaqsvmncuzmwv2r6iuzc5t7ur6ugdhephz7ydftypksjidpsylbq", "contract/valory/staking_token/0.1.0": "bafybeifrvtkofw5c26b3irm6izqfdpik6vpjhm6hqwcdzx333h6vhdanai", "contract/valory/staking_activity_checker/0.1.0": "bafybeibjzsi2r5b6xd4iwl4wbwldptnynryzsdpifym4mkv32ynswx22ou", - "skill/valory/liquidity_trader_abci/0.1.0": "bafybeihtca6gtyjibj6wkrcdmx3fb3a3bkpdgsphwevkatagxrbqvh6fd4", - "skill/valory/optimus_abci/0.1.0": "bafybeifjpvqz2m7qhztib4xcjpbjkuiutrot22flqclg36amvqvrp5ra3e", - "agent/valory/optimus/0.1.0": "bafybeida2scmw3qune3n6ru7tuzquuc3mxs2cfivzcncrtlj4ziadv4sqy", - "service/valory/optimus/0.1.0": "bafybeidlfxklqbwrba5xdbigchkl5dcqcrlpzbrkem62jbzr5yghwe7tgu" + "skill/valory/liquidity_trader_abci/0.1.0": "bafybeib2r7z5ufnb5nmtvob4bejb6ql3rrocqb6oinsze7xcaa33ulibye", + "skill/valory/optimus_abci/0.1.0": "bafybeider6qgvmw64zzigonfp6coaap3gubredzqcz7oq7luf3aqkqincm", + "agent/valory/optimus/0.1.0": "bafybeibgilhnvikn3iol772xignjqpfrnz2rzwmpzn2qcr5o7mtnbpb7hq", + "service/valory/optimus/0.1.0": "bafybeibne2lvvy5v2wvs7jtpxzbucodhhfmzjwp23vklcjjy6mphc2i4ge" }, "third_party": { "custom/eightballer/rsi_strategy/0.1.0": "bafybeigbofp2nqwcxu3rlkuugpc3w6ils3u7glse7c335rddcqg56ybh34", @@ -50,10 +50,10 @@ "connection/valory/ledger/0.19.0": "bafybeigntoericenpzvwejqfuc3kqzo2pscs76qoygg5dbj6f4zxusru5e", "connection/valory/p2p_libp2p_client/0.1.0": "bafybeid3xg5k2ol5adflqloy75ibgljmol6xsvzvezebsg7oudxeeolz7e", "connection/valory/http_server/0.22.0": "bafybeihpgu56ovmq4npazdbh6y6ru5i7zuv6wvdglpxavsckyih56smu7m", - "skill/valory/trader_abci/0.1.0": "bafybeigco23fstumklkdfaatafca6eyv2vcwzabdsrzhlpmkyl7d3nbgv4", - "skill/valory/strategy_evaluator_abci/0.1.0": "bafybeibsk56r3dd5tza5yl2ltbonxdkb7zgfrupgiq3iyzr2ywqycskvz4", - "skill/valory/market_data_fetcher_abci/0.1.0": "bafybeibaf2ubj56busl7ngaamyjtm3tqsxthluuve6znxeogoxo7ta6b3y", - "skill/valory/portfolio_tracker_abci/0.1.0": "bafybeidprqeiomlvs6e5nvcqr2hnntwiuwyaxifidznjowoexx65yoplmq", + "skill/valory/trader_abci/0.1.0": "bafybeifspduqypugmtrpirg3ofpz5wcenid24hthce25npqiatmoqozkya", + "skill/valory/strategy_evaluator_abci/0.1.0": "bafybeidbib2sdjgq3354emzcc22zzgdm4z5hjdz2gbvcqabw5j4rdgcx5a", + "skill/valory/market_data_fetcher_abci/0.1.0": "bafybeies3ib3ft3tqepogstxjarq3ku4xepnfozyvg2rur5ztq4afchgcq", + "skill/valory/portfolio_tracker_abci/0.1.0": "bafybeihigssk7zxefcr44hpxwrnsa6ho53zyulqhepxvm3idsywzd6w5wq", "skill/valory/trader_decision_maker_abci/0.1.0": "bafybeicskzcqr7dd4d5ulwss5u3hsx2wvcxatugtxiuja2neobbr3tqtkm", "skill/valory/abstract_abci/0.1.0": "bafybeidz54kvxhbdmpruzguuzzq7bjg4pekjb5amqobkxoy4oqknnobopu", "skill/valory/reset_pause_abci/0.1.0": "bafybeif4lgvbzsmzljesxbphycdv52ka7qnihyjrjpfaseclxadcmm6yiq", diff --git a/pyproject.toml b/pyproject.toml index f41adc0..7b6d139 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,10 +60,10 @@ jsonschema = "<4.4.0,>=4.3.0" pandas = ">=1.3.0" pyalgotrade = "==0.20" lyra-v2-client = ">=0.2.9" -balpy = {path = "third_party/balpy"} +balpy = "0.0.7" numpy = "==1.26.1" dateparser = ">=1.1.1" -multicaller = {path = "third_party/multicaller"} +open-multicaller = "==0.2.0" [tool.poetry.group.dev.dependencies] pytest-xprocess = ">=0.18.1,<0.19.0" diff --git a/tox.ini b/tox.ini index 0e54d24..b3ce566 100644 --- a/tox.ini +++ b/tox.ini @@ -38,6 +38,8 @@ deps = pytest==7.2.1 openapi-core==0.15.0 openapi-spec-validator<0.5.0,>=0.4.0 + open-multicaller==0.2.0 + balpy==0.0.7 [extra-deps] deps = From c34d5e9c148daf048d1ca99b3325b7a531e96df7 Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Tue, 29 Oct 2024 12:54:04 +0530 Subject: [PATCH 25/41] chore: Update connection in packages --- packages/packages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/packages.json b/packages/packages.json index 4d84e82..79cb176 100644 --- a/packages/packages.json +++ b/packages/packages.json @@ -43,7 +43,7 @@ "contract/valory/gnosis_safe_proxy_factory/0.1.0": "bafybeicpcpyurm7gxir2gnlsgzeirzomkhcbnzr5txk67zdf4mmg737rtu", "contract/valory/multisend/0.1.0": "bafybeig5byt5urg2d2bsecufxe5ql7f4mezg3mekfleeh32nmuusx66p4y", "contract/valory/gnosis_safe/0.1.0": "bafybeib375xmvcplw7ageic2np3hq4yqeijrvd5kl7rrdnyvswats6ngmm", - "connection/eightballer/dcxt/0.1.0": "bafybeihnaxpiki57ivphf24qpie5g6rxwnztbrymutlw4yuyjxzvj3pdry", + "connection/eightballer/dcxt/0.1.0": "bafybeiexzq2nvwi5mpgsnbfjorfd4hr5v56ocy4xshjyqcv43nwcbo6zk4", "connection/valory/abci/0.1.0": "bafybeiejymu4ul62zx6weoibnlsrfprfpjnplhjefz6sr6izgdr4sajlnu", "connection/valory/http_client/0.23.0": "bafybeihi772xgzpqeipp3fhmvpct4y6e6tpjp4sogwqrnf3wqspgeilg4u", "connection/valory/ipfs/0.1.0": "bafybeiegnapkvkamis47v5ioza2haerrjdzzb23rptpmcydyneas7jc2wm", From 6acdc3111336be5eb76a8f7139e92d0ed234d39b Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Tue, 29 Oct 2024 14:07:48 +0530 Subject: [PATCH 26/41] chore: Update pakages --- packages/packages.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/packages.json b/packages/packages.json index 79cb176..c05bdd4 100644 --- a/packages/packages.json +++ b/packages/packages.json @@ -9,9 +9,9 @@ "contract/valory/staking_token/0.1.0": "bafybeifrvtkofw5c26b3irm6izqfdpik6vpjhm6hqwcdzx333h6vhdanai", "contract/valory/staking_activity_checker/0.1.0": "bafybeibjzsi2r5b6xd4iwl4wbwldptnynryzsdpifym4mkv32ynswx22ou", "skill/valory/liquidity_trader_abci/0.1.0": "bafybeib2r7z5ufnb5nmtvob4bejb6ql3rrocqb6oinsze7xcaa33ulibye", - "skill/valory/optimus_abci/0.1.0": "bafybeider6qgvmw64zzigonfp6coaap3gubredzqcz7oq7luf3aqkqincm", - "agent/valory/optimus/0.1.0": "bafybeibgilhnvikn3iol772xignjqpfrnz2rzwmpzn2qcr5o7mtnbpb7hq", - "service/valory/optimus/0.1.0": "bafybeibne2lvvy5v2wvs7jtpxzbucodhhfmzjwp23vklcjjy6mphc2i4ge" + "skill/valory/optimus_abci/0.1.0": "bafybeib5ctllbumgne5j6xaenkss7lwmeyn5bkcrftap5cyf6gdwbo4rhq", + "agent/valory/optimus/0.1.0": "bafybeiggdox3555xcvvzzlrls74sjzrz4imh3ki4srpw6emgusvqumf3bm", + "service/valory/optimus/0.1.0": "bafybeifmgufluiixnao3hnf7uz5bgi2qvhr4m6mbul2gghjghveagffemi" }, "third_party": { "custom/eightballer/rsi_strategy/0.1.0": "bafybeigbofp2nqwcxu3rlkuugpc3w6ils3u7glse7c335rddcqg56ybh34", @@ -50,7 +50,6 @@ "connection/valory/ledger/0.19.0": "bafybeigntoericenpzvwejqfuc3kqzo2pscs76qoygg5dbj6f4zxusru5e", "connection/valory/p2p_libp2p_client/0.1.0": "bafybeid3xg5k2ol5adflqloy75ibgljmol6xsvzvezebsg7oudxeeolz7e", "connection/valory/http_server/0.22.0": "bafybeihpgu56ovmq4npazdbh6y6ru5i7zuv6wvdglpxavsckyih56smu7m", - "skill/valory/trader_abci/0.1.0": "bafybeifspduqypugmtrpirg3ofpz5wcenid24hthce25npqiatmoqozkya", "skill/valory/strategy_evaluator_abci/0.1.0": "bafybeidbib2sdjgq3354emzcc22zzgdm4z5hjdz2gbvcqabw5j4rdgcx5a", "skill/valory/market_data_fetcher_abci/0.1.0": "bafybeies3ib3ft3tqepogstxjarq3ku4xepnfozyvg2rur5ztq4afchgcq", "skill/valory/portfolio_tracker_abci/0.1.0": "bafybeihigssk7zxefcr44hpxwrnsa6ho53zyulqhepxvm3idsywzd6w5wq", From 64fba8948ad8cdc0a554242ba1d68d6bf037d3a8 Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Tue, 29 Oct 2024 14:11:05 +0530 Subject: [PATCH 27/41] chore: Remove third-party submodules for balpy and multicaller --- .gitmodules | 8 -------- third_party/balpy | 1 - third_party/multicaller | 1 - 3 files changed, 10 deletions(-) delete mode 100644 .gitmodules delete mode 160000 third_party/balpy delete mode 160000 third_party/multicaller diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 0356e2b..0000000 --- a/.gitmodules +++ /dev/null @@ -1,8 +0,0 @@ - - -[submodule "third_party/balpy"] - path = third_party/balpy - url = https://github.com/8ball030/balpy.git -[submodule "third_party/multicaller"] - path = third_party/multicaller - url = https://github.com/8ball030/multicaller.git diff --git a/third_party/balpy b/third_party/balpy deleted file mode 160000 index 4269105..0000000 --- a/third_party/balpy +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 42691053c56f7740e2b1eb7981c3db99907ae362 diff --git a/third_party/multicaller b/third_party/multicaller deleted file mode 160000 index 0d4953d..0000000 --- a/third_party/multicaller +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0d4953d527cd2282b30646881fa02ec5f5e9a678 From 52baec4caed637a0079aec121f99d4a18ec52445 Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Tue, 29 Oct 2024 15:26:19 +0530 Subject: [PATCH 28/41] chore: updating toml and tox.ini for ci issues --- poetry.lock | 763 +++++++++++++++++++++++++++++++++---------------- pyproject.toml | 2 +- tox.ini | 13 +- 3 files changed, 528 insertions(+), 250 deletions(-) diff --git a/poetry.lock b/poetry.lock index 7bf2a4a..8012124 100644 --- a/poetry.lock +++ b/poetry.lock @@ -193,6 +193,17 @@ files = [ [package.dependencies] jsonalias = "0.1.1" +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + [[package]] name = "anyio" version = "4.6.2.post1" @@ -267,29 +278,6 @@ files = [ {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"}, ] -[[package]] -name = "balpy" -version = "0.0.0a84" -description = "Balancer V2 Python API" -optional = false -python-versions = ">=3.10,<4" -files = [] -develop = false - -[package.dependencies] -Cython = ">=0.29.24" -cytoolz = "0.11.2" -eth-abi = ">=2.1.1" -gql = ">=2.0.0" -jstyleson = ">=0.0.2" -multicaller = {path = "../multicaller"} -requests = ">=2.25.1" -web3 = "~6" - -[package.source] -type = "directory" -url = "third_party/balpy" - [[package]] name = "base58" version = "2.1.1" @@ -1283,12 +1271,97 @@ files = [ [[package]] name = "cytoolz" -version = "0.11.2" +version = "1.0.0" description = "Cython implementation of Toolz: High performance functional utilities" optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" files = [ - {file = "cytoolz-0.11.2.tar.gz", hash = "sha256:ea23663153806edddce7e4153d1d407d62357c05120a4e8485bddf1bd5ab22b4"}, + {file = "cytoolz-1.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ecf5a887acb8f079ab1b81612b1c889bcbe6611aa7804fd2df46ed310aa5a345"}, + {file = "cytoolz-1.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ef0ef30c1e091d4d59d14d8108a16d50bd227be5d52a47da891da5019ac2f8e4"}, + {file = "cytoolz-1.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7df2dfd679f0517a96ced1cdd22f5c6c6aeeed28d928a82a02bf4c3fd6fd7ac4"}, + {file = "cytoolz-1.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c51452c938e610f57551aa96e34924169c9100c0448bac88c2fb395cbd3538c"}, + {file = "cytoolz-1.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6433f03910c5e5345d82d6299457c26bf33821224ebb837c6b09d9cdbc414a6c"}, + {file = "cytoolz-1.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:389ec328bb535f09e71dfe658bf0041f17194ca4cedaacd39bafe7893497a819"}, + {file = "cytoolz-1.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c64658e1209517ce4b54c1c9269a508b289d8d55fc742760e4b8579eacf09a33"}, + {file = "cytoolz-1.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f6039a9bd5bb988762458b9ca82b39e60ca5e5baae2ba93913990dcc5d19fa88"}, + {file = "cytoolz-1.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:85c9c8c4465ed1b2c8d67003809aec9627b129cb531d2f6cf0bbfe39952e7e4d"}, + {file = "cytoolz-1.0.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:49375aad431d76650f94877afb92f09f58b6ff9055079ef4f2cd55313f5a1b39"}, + {file = "cytoolz-1.0.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:4c45106171c824a61e755355520b646cb35a1987b34bbf5789443823ee137f63"}, + {file = "cytoolz-1.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3b319a7f0fed5db07d189db4046162ebc183c108df3562a65ba6ebe862d1f634"}, + {file = "cytoolz-1.0.0-cp310-cp310-win32.whl", hash = "sha256:9770e1b09748ad0d751853d994991e2592a9f8c464a87014365f80dac2e83faa"}, + {file = "cytoolz-1.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:20194dd02954c00c1f0755e636be75a20781f91a4ac9270c7f747e82d3c7f5a5"}, + {file = "cytoolz-1.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dffc22fd2c91be64dbdbc462d0786f8e8ac9a275cfa1869a1084d1867d4f67e0"}, + {file = "cytoolz-1.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a99e7e29274e293f4ffe20e07f76c2ac753a78f1b40c1828dfc54b2981b2f6c4"}, + {file = "cytoolz-1.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c507a3e0a45c41d66b43f96797290d75d1e7a8549aa03a4a6b8854fdf3f7b8d8"}, + {file = "cytoolz-1.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:643a593ec272ef7429099e1182a22f64ec2696c00d295d2a5be390db1b7ff176"}, + {file = "cytoolz-1.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ce38e2e42cbae30446190c59b92a8a9029e1806fd79eaf88f48b0fe33003893"}, + {file = "cytoolz-1.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810a6a168b8c5ecb412fbae3dd6f7ed6c6253a63caf4174ee9794ebd29b2224f"}, + {file = "cytoolz-1.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ce8a2a85c0741c1b19b16e6782c4a5abc54c3caecda66793447112ab2fa9884"}, + {file = "cytoolz-1.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ea4ac72e6b830861035c4c7999af8e55813f57c6d1913a3d93cc4a6babc27bf7"}, + {file = "cytoolz-1.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a09cdfb21dfb38aa04df43e7546a41f673377eb5485da88ceb784e327ec7603b"}, + {file = "cytoolz-1.0.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:658dd85deb375ff7af990a674e5c9058cef1c9d1f5dc89bc87b77be499348144"}, + {file = "cytoolz-1.0.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9715d1ff5576919d10b68f17241375f6a1eec8961c25b78a83e6ef1487053f39"}, + {file = "cytoolz-1.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f370a1f1f1afc5c1c8cc5edc1cfe0ba444263a0772af7ce094be8e734f41769d"}, + {file = "cytoolz-1.0.0-cp311-cp311-win32.whl", hash = "sha256:dbb2ec1177dca700f3db2127e572da20de280c214fc587b2a11c717fc421af56"}, + {file = "cytoolz-1.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:0983eee73df86e54bb4a79fcc4996aa8b8368fdbf43897f02f9c3bf39c4dc4fb"}, + {file = "cytoolz-1.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:10e3986066dc379e30e225b230754d9f5996aa8d84c2accc69c473c21d261e46"}, + {file = "cytoolz-1.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:16576f1bb143ee2cb9f719fcc4b845879fb121f9075c7c5e8a5ff4854bd02fc6"}, + {file = "cytoolz-1.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3faa25a1840b984315e8b3ae517312375f4273ffc9a2f035f548b7f916884f37"}, + {file = "cytoolz-1.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:781fce70a277b20fd95dc66811d1a97bb07b611ceea9bda8b7dd3c6a4b05d59a"}, + {file = "cytoolz-1.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a562c25338eb24d419d1e80a7ae12133844ce6fdeb4ab54459daf250088a1b2"}, + {file = "cytoolz-1.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f29d8330aaf070304f7cd5cb7e73e198753624eb0aec278557cccd460c699b5b"}, + {file = "cytoolz-1.0.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:98a96c54aa55ed9c7cdb23c2f0df39a7b4ee518ac54888480b5bdb5ef69c7ef0"}, + {file = "cytoolz-1.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:287d6d7f475882c2ddcbedf8da9a9b37d85b77690779a2d1cdceb5ae3998d52e"}, + {file = "cytoolz-1.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:05a871688df749b982839239fcd3f8ec3b3b4853775d575ff9cd335fa7c75035"}, + {file = "cytoolz-1.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:28bb88e1e2f7d6d4b8e0890b06d292c568984d717de3e8381f2ca1dd12af6470"}, + {file = "cytoolz-1.0.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:576a4f1fc73d8836b10458b583f915849da6e4f7914f4ecb623ad95c2508cad5"}, + {file = "cytoolz-1.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:509ed3799c47e4ada14f63e41e8f540ac6e2dab97d5d7298934e6abb9d3830ec"}, + {file = "cytoolz-1.0.0-cp312-cp312-win32.whl", hash = "sha256:9ce25f02b910630f6dc2540dd1e26c9326027ddde6c59f8cab07c56acc70714c"}, + {file = "cytoolz-1.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:7e53cfcce87e05b7f0ae2fb2b3e5820048cd0bb7b701e92bd8f75c9fbb7c9ae9"}, + {file = "cytoolz-1.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7d56569dfe67a39ce74ffff0dc12cf0a3d1aae709667a303fe8f2dd5fd004fdf"}, + {file = "cytoolz-1.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:035c8bb4706dcf93a89fb35feadff67e9301935bf6bb864cd2366923b69d9a29"}, + {file = "cytoolz-1.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27c684799708bdc7ee7acfaf464836e1b4dec0996815c1d5efd6a92a4356a562"}, + {file = "cytoolz-1.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44ab57cfc922b15d94899f980d76759ef9e0256912dfab70bf2561bea9cd5b19"}, + {file = "cytoolz-1.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:478af5ecc066da093d7660b23d0b465a7f44179739937afbded8af00af412eb6"}, + {file = "cytoolz-1.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da1f82a7828a42468ea2820a25b6e56461361390c29dcd4d68beccfa1b71066b"}, + {file = "cytoolz-1.0.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c371b3114d38ee717780b239179e88d5d358fe759a00dcf07691b8922bbc762"}, + {file = "cytoolz-1.0.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:90b343b2f3b3e77c3832ba19b0b17e95412a5b2e715b05c23a55ba525d1fca49"}, + {file = "cytoolz-1.0.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89a554a9ba112403232a54e15e46ff218b33020f3f45c4baf6520ab198b7ad93"}, + {file = "cytoolz-1.0.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:0d603f5e2b1072166745ecdd81384a75757a96a704a5642231eb51969f919d5f"}, + {file = "cytoolz-1.0.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:122ef2425bd3c0419e6e5260d0b18cd25cf74de589cd0184e4a63b24a4641e2e"}, + {file = "cytoolz-1.0.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8819f1f97ebe36efcaf4b550e21677c46ac8a41bed482cf66845f377dd20700d"}, + {file = "cytoolz-1.0.0-cp38-cp38-win32.whl", hash = "sha256:fcddbb853770dd6e270d89ea8742f0aa42c255a274b9e1620eb04e019b79785e"}, + {file = "cytoolz-1.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:ca526905a014a38cc23ae78635dc51d0462c5c24425b22c08beed9ff2ee03845"}, + {file = "cytoolz-1.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:05df5ff1cdd198fb57e7368623662578c950be0b14883cadfb9ee4098415e1e5"}, + {file = "cytoolz-1.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:04a84778f48ebddb26948971dc60948907c876ba33b13f9cbb014fe65b341fc2"}, + {file = "cytoolz-1.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f65283b618b4c4df759f57bcf8483865a73f7f268e6d76886c743407c8d26c1c"}, + {file = "cytoolz-1.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:388cd07ee9a9e504c735a0a933e53c98586a1c301a64af81f7aa7ff40c747520"}, + {file = "cytoolz-1.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:06d09e9569cfdfc5c082806d4b4582db8023a3ce034097008622bcbac7236f38"}, + {file = "cytoolz-1.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9502bd9e37779cc9893cbab515a474c2ab6af61ed22ac2f7e16033db18fcaa85"}, + {file = "cytoolz-1.0.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:364c2fda148def38003b2c86e8adde1d2aab12411dd50872c244a815262e2fda"}, + {file = "cytoolz-1.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9b2e945617325242687189966335e785dc0fae316f4c1825baacf56e5a97e65f"}, + {file = "cytoolz-1.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0f16907fdc724c55b16776bdb7e629deae81d500fe48cfc3861231753b271355"}, + {file = "cytoolz-1.0.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d3206c81ca3ba2d7b8fe78f2e116e3028e721148be753308e88dcbbc370bca52"}, + {file = "cytoolz-1.0.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:becce4b13e110b5ac6b23753dcd0c977f4fdccffa31898296e13fd1109e517e3"}, + {file = "cytoolz-1.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:69a7e5e98fd446079b8b8ec5987aec9a31ec3570a6f494baefa6800b783eaf22"}, + {file = "cytoolz-1.0.0-cp39-cp39-win32.whl", hash = "sha256:b1707b6c3a91676ac83a28a231a14b337dbb4436b937e6b3e4fd44209852a48b"}, + {file = "cytoolz-1.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:11d48b8521ef5fe92e099f4fc00717b5d0789c3c90d5d84031b6d3b17dee1700"}, + {file = "cytoolz-1.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e672712d5dc3094afc6fb346dd4e9c18c1f3c69608ddb8cf3b9f8428f9c26a5c"}, + {file = "cytoolz-1.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86fb208bfb7420e1d0d20065d661310e4a8a6884851d4044f47d37ed4cd7410e"}, + {file = "cytoolz-1.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6dbe5fe3b835859fc559eb59bf2775b5a108f7f2cfab0966f3202859d787d8fd"}, + {file = "cytoolz-1.0.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cace092dfda174eed09ed871793beb5b65633963bcda5b1632c73a5aceea1ce"}, + {file = "cytoolz-1.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f7a9d816af3be9725c70efe0a6e4352a45d3877751b395014b8eb2f79d7d8d9d"}, + {file = "cytoolz-1.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:caa7ef840847a23b379e6146760e3a22f15f445656af97e55a435c592125cfa5"}, + {file = "cytoolz-1.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:921082fff09ff6e40c12c87b49be044492b2d6bb01d47783995813b76680c7b2"}, + {file = "cytoolz-1.0.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a32f1356f3b64dda883583383966948604ac69ca0b7fbcf5f28856e5f9133b4e"}, + {file = "cytoolz-1.0.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9af793b1738e4191d15a92e1793f1ffea9f6461022c7b2442f3cb1ea0a4f758a"}, + {file = "cytoolz-1.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:51dfda3983fcc59075c534ce54ca041bb3c80e827ada5d4f25ff7b4049777f94"}, + {file = "cytoolz-1.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:acfb8780c04d29423d14aaab74cd1b7b4beaba32f676e7ace02c9acfbf532aba"}, + {file = "cytoolz-1.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99f39dcc46416dca3eb23664b73187b77fb52cd8ba2ddd8020a292d8f449db67"}, + {file = "cytoolz-1.0.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0d56b3721977806dcf1a68b0ecd56feb382fdb0f632af1a9fc5ab9b662b32c6"}, + {file = "cytoolz-1.0.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d346620abc8c83ae634136e700432ad6202faffcc24c5ab70b87392dcda8a1"}, + {file = "cytoolz-1.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:df0c81197fc130de94c09fc6f024a6a19c98ba8fe55c17f1e45ebba2e9229079"}, + {file = "cytoolz-1.0.0.tar.gz", hash = "sha256:eb453b30182152f9917a5189b7d99046b6ce90cdf8aeb0feff4b2683e600defd"}, ] [package.dependencies] @@ -1708,88 +1781,103 @@ woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] [[package]] name = "frozenlist" -version = "1.4.1" +version = "1.5.0" description = "A list-like structure which implements collections.abc.MutableSequence" optional = false python-versions = ">=3.8" files = [ - {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, - {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, - {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, - {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, - {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, - {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, - {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, - {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, - {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, - {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, - {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, - {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, - {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, - {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, - {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, + {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"}, + {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"}, + {file = "frozenlist-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5"}, + {file = "frozenlist-1.5.0-cp310-cp310-win32.whl", hash = "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb"}, + {file = "frozenlist-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf"}, + {file = "frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942"}, + {file = "frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f"}, + {file = "frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8"}, + {file = "frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03"}, + {file = "frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c"}, + {file = "frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e"}, + {file = "frozenlist-1.5.0-cp38-cp38-win32.whl", hash = "sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723"}, + {file = "frozenlist-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c"}, + {file = "frozenlist-1.5.0-cp39-cp39-win32.whl", hash = "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3"}, + {file = "frozenlist-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0"}, + {file = "frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3"}, + {file = "frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817"}, ] [[package]] @@ -2373,17 +2461,17 @@ test = ["pytest"] [[package]] name = "lyra-v2-client" -version = "0.2.10" +version = "0.2.11" description = "" optional = false -python-versions = "<=3.11.9,>=3.8.1" +python-versions = "<3.12,>=3.8.1" files = [ - {file = "lyra_v2_client-0.2.10-py3-none-any.whl", hash = "sha256:c10f9d38227e43294bfbabce4ecfd0460e82ba05e2fad0e28e739c0140c843a9"}, - {file = "lyra_v2_client-0.2.10.tar.gz", hash = "sha256:6fbb200c9f8ae9c9b26c6a1d7de3ef6bc84226a8a1942af08b7f48981187fa1c"}, + {file = "lyra_v2_client-0.2.11-py3-none-any.whl", hash = "sha256:27fe1feafd4e9bb0ad91894e89dcd9a5edf9523ea29d9e8a00ca2b3ceb99abbd"}, + {file = "lyra_v2_client-0.2.11.tar.gz", hash = "sha256:bb140b0d532720ac00577da5baf19041d018de083e30fde62290d0938eeca358"}, ] [package.dependencies] -pandas = ">=1,<2" +pandas = ">=1,<=3" python-dotenv = ">=0.14.0,<0.18.0" requests = ">=2,<3" rich-click = ">=1.7.1,<2.0.0" @@ -2597,22 +2685,6 @@ netaddr = "*" six = "*" varint = "*" -[[package]] -name = "multicaller" -version = "0.1.7" -description = "web3py multicaller simplified interface" -optional = false -python-versions = ">=3.8.1,<4" -files = [] -develop = false - -[package.dependencies] -web3 = "~6" - -[package.source] -type = "directory" -url = "third_party/multicaller" - [[package]] name = "multidict" version = "6.1.0" @@ -2962,6 +3034,42 @@ werkzeug = "2.0.3" all = ["click (>=8.1.0,<9)", "coverage (>=6.4.4,<8.0.0)", "open-aea-cli-ipfs (==1.57.0)", "pytest (>=7.0.0,<7.3.0)", "python-dotenv (>=0.14.5,<0.22.0)", "texttable (==1.6.7)"] cli = ["click (>=8.1.0,<9)", "coverage (>=6.4.4,<8.0.0)", "open-aea-cli-ipfs (==1.57.0)", "pytest (>=7.0.0,<7.3.0)", "python-dotenv (>=0.14.5,<0.22.0)", "texttable (==1.6.7)"] +[[package]] +name = "open-balpy" +version = "0.0.7" +description = "Balancer V2 Python API" +optional = false +python-versions = "<4,>=3.9" +files = [ + {file = "open_balpy-0.0.7-py3-none-any.whl", hash = "sha256:995d9d3ad3decd90cab479686b47364144f868039c6b12fc8ee4f9c3eaa35378"}, + {file = "open_balpy-0.0.7.tar.gz", hash = "sha256:3cb05743b19861d80f511670e59a66fc6853f55121b57c402c62adad6e81243b"}, +] + +[package.dependencies] +Cython = ">=0.29.24" +cytoolz = ">=0.11.2" +eth-abi = ">=2.1.1" +gql = ">=2.0.0" +jstyleson = ">=0.0.2" +open-multicaller = ">=0.2.0" +requests = ">=2.25.1" +requests-toolbelt = ">=1.0.0,<2.0.0" +web3 = ">=6,<7" + +[[package]] +name = "open-multicaller" +version = "0.2.0" +description = "web3py multicaller simplified interface" +optional = false +python-versions = "<4,>=3.8.1" +files = [ + {file = "open_multicaller-0.2.0-py3-none-any.whl", hash = "sha256:16d283df9b3644a67d7597734b22416ecb9e2c1f8310487fe035e4c8a23a5e2f"}, + {file = "open_multicaller-0.2.0.tar.gz", hash = "sha256:742889c86c1b384eddfbec8d05edfb9466ff3a2bc3e641f3252c2576a01e2b8f"}, +] + +[package.dependencies] +web3 = ">=6,<7" + [[package]] name = "openapi-core" version = "0.15.0" @@ -3041,50 +3149,88 @@ files = [ [[package]] name = "pandas" -version = "1.5.3" +version = "2.2.3" description = "Powerful data structures for data analysis, time series, and statistics" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3749077d86e3a2f0ed51367f30bf5b82e131cc0f14260c4d3e499186fccc4406"}, - {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:972d8a45395f2a2d26733eb8d0f629b2f90bebe8e8eddbb8829b180c09639572"}, - {file = "pandas-1.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50869a35cbb0f2e0cd5ec04b191e7b12ed688874bd05dd777c19b28cbea90996"}, - {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3ac844a0fe00bfaeb2c9b51ab1424e5c8744f89860b138434a363b1f620f354"}, - {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0a56cef15fd1586726dace5616db75ebcfec9179a3a55e78f72c5639fa2a23"}, - {file = "pandas-1.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:478ff646ca42b20376e4ed3fa2e8d7341e8a63105586efe54fa2508ee087f328"}, - {file = "pandas-1.5.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6973549c01ca91ec96199e940495219c887ea815b2083722821f1d7abfa2b4dc"}, - {file = "pandas-1.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c39a8da13cede5adcd3be1182883aea1c925476f4e84b2807a46e2775306305d"}, - {file = "pandas-1.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f76d097d12c82a535fda9dfe5e8dd4127952b45fea9b0276cb30cca5ea313fbc"}, - {file = "pandas-1.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e474390e60ed609cec869b0da796ad94f420bb057d86784191eefc62b65819ae"}, - {file = "pandas-1.5.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f2b952406a1588ad4cad5b3f55f520e82e902388a6d5a4a91baa8d38d23c7f6"}, - {file = "pandas-1.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc4c368f42b551bf72fac35c5128963a171b40dce866fb066540eeaf46faa003"}, - {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:14e45300521902689a81f3f41386dc86f19b8ba8dd5ac5a3c7010ef8d2932813"}, - {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9842b6f4b8479e41968eced654487258ed81df7d1c9b7b870ceea24ed9459b31"}, - {file = "pandas-1.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:26d9c71772c7afb9d5046e6e9cf42d83dd147b5cf5bcb9d97252077118543792"}, - {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fbcb19d6fceb9e946b3e23258757c7b225ba450990d9ed63ccceeb8cae609f7"}, - {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:565fa34a5434d38e9d250af3c12ff931abaf88050551d9fbcdfafca50d62babf"}, - {file = "pandas-1.5.3-cp38-cp38-win32.whl", hash = "sha256:87bd9c03da1ac870a6d2c8902a0e1fd4267ca00f13bc494c9e5a9020920e1d51"}, - {file = "pandas-1.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:41179ce559943d83a9b4bbacb736b04c928b095b5f25dd2b7389eda08f46f373"}, - {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c74a62747864ed568f5a82a49a23a8d7fe171d0c69038b38cedf0976831296fa"}, - {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c4c00e0b0597c8e4f59e8d461f797e5d70b4d025880516a8261b2817c47759ee"}, - {file = "pandas-1.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a50d9a4336a9621cab7b8eb3fb11adb82de58f9b91d84c2cd526576b881a0c5a"}, - {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd05f7783b3274aa206a1af06f0ceed3f9b412cf665b7247eacd83be41cf7bf0"}, - {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f69c4029613de47816b1bb30ff5ac778686688751a5e9c99ad8c7031f6508e5"}, - {file = "pandas-1.5.3-cp39-cp39-win32.whl", hash = "sha256:7cec0bee9f294e5de5bbfc14d0573f65526071029d036b753ee6507d2a21480a"}, - {file = "pandas-1.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:dfd681c5dc216037e0b0a2c821f5ed99ba9f03ebcf119c7dac0e9a7b960b9ec9"}, - {file = "pandas-1.5.3.tar.gz", hash = "sha256:74a3fd7e5a7ec052f183273dc7b0acd3a863edf7520f5d3a1765c04ffdb3b0b1"}, + {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, + {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, + {file = "pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed"}, + {file = "pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57"}, + {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42"}, + {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f"}, + {file = "pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645"}, + {file = "pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039"}, + {file = "pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd"}, + {file = "pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698"}, + {file = "pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc"}, + {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3"}, + {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32"}, + {file = "pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5"}, + {file = "pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9"}, + {file = "pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4"}, + {file = "pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3"}, + {file = "pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319"}, + {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8"}, + {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a"}, + {file = "pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13"}, + {file = "pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015"}, + {file = "pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28"}, + {file = "pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0"}, + {file = "pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24"}, + {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659"}, + {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb"}, + {file = "pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d"}, + {file = "pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468"}, + {file = "pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18"}, + {file = "pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2"}, + {file = "pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4"}, + {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d"}, + {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a"}, + {file = "pandas-2.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39"}, + {file = "pandas-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30"}, + {file = "pandas-2.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c"}, + {file = "pandas-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c"}, + {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea"}, + {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761"}, + {file = "pandas-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e"}, + {file = "pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667"}, ] [package.dependencies] numpy = [ - {version = ">=1.21.0", markers = "python_version >= \"3.10\" and python_version < \"3.11\""}, - {version = ">=1.23.2", markers = "python_version >= \"3.11\""}, + {version = ">=1.22.4", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, ] -python-dateutil = ">=2.8.1" +python-dateutil = ">=2.8.2" pytz = ">=2020.1" +tzdata = ">=2022.7" [package.extras] -test = ["hypothesis (>=5.5.3)", "pytest (>=6.0)", "pytest-xdist (>=1.31)"] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] +aws = ["s3fs (>=2022.11.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] +compression = ["zstandard (>=0.19.0)"] +computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] +feather = ["pyarrow (>=10.0.1)"] +fss = ["fsspec (>=2022.11.0)"] +gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] +hdf5 = ["tables (>=3.8.0)"] +html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] +mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=10.0.1)"] +performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] +plot = ["matplotlib (>=3.6.3)"] +postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +pyarrow = ["pyarrow (>=10.0.1)"] +spss = ["pyreadstat (>=1.2.0)"] +sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.9.2)"] [[package]] name = "paramiko" @@ -3595,6 +3741,127 @@ files = [ {file = "pycryptodome-3.18.0.tar.gz", hash = "sha256:c9adee653fc882d98956e33ca2c1fb582e23a8af7ac82fee75bd6113c55a0413"}, ] +[[package]] +name = "pydantic" +version = "2.9.2" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, + {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.23.4" +typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""} + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.23.4" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, + {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, + {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, + {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, + {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, + {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, + {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, + {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, + {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, + {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, + {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, + {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, + {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, + {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "pygments" version = "2.18.0" @@ -4169,13 +4436,13 @@ idna2008 = ["idna"] [[package]] name = "rich" -version = "13.9.2" +version = "13.9.3" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" files = [ - {file = "rich-13.9.2-py3-none-any.whl", hash = "sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1"}, - {file = "rich-13.9.2.tar.gz", hash = "sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c"}, + {file = "rich-13.9.3-py3-none-any.whl", hash = "sha256:9836f5096eb2172c9e77df411c1b009bace4193d6a481d534fea75ebba758283"}, + {file = "rich-13.9.3.tar.gz", hash = "sha256:bc1e01b899537598cf02579d2b9f4a415104d3fc439313a7a2c165d76557a08e"}, ] [package.dependencies] @@ -4648,13 +4915,13 @@ files = [ [[package]] name = "virtualenv" -version = "20.27.0" +version = "20.27.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" files = [ - {file = "virtualenv-20.27.0-py3-none-any.whl", hash = "sha256:44a72c29cceb0ee08f300b314848c86e57bf8d1f13107a5e671fb9274138d655"}, - {file = "virtualenv-20.27.0.tar.gz", hash = "sha256:2ca56a68ed615b8fe4326d11a0dca5dfbe8fd68510fb6c6349163bed3c15f2b2"}, + {file = "virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4"}, + {file = "virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba"}, ] [package.dependencies] @@ -4861,93 +5128,93 @@ files = [ [[package]] name = "yarl" -version = "1.16.0" +version = "1.17.0" description = "Yet another URL library" optional = false python-versions = ">=3.9" files = [ - {file = "yarl-1.16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32468f41242d72b87ab793a86d92f885355bcf35b3355aa650bfa846a5c60058"}, - {file = "yarl-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:234f3a3032b505b90e65b5bc6652c2329ea7ea8855d8de61e1642b74b4ee65d2"}, - {file = "yarl-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a0296040e5cddf074c7f5af4a60f3fc42c0237440df7bcf5183be5f6c802ed5"}, - {file = "yarl-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de6c14dd7c7c0badba48157474ea1f03ebee991530ba742d381b28d4f314d6f3"}, - {file = "yarl-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b140e532fe0266003c936d017c1ac301e72ee4a3fd51784574c05f53718a55d8"}, - {file = "yarl-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:019f5d58093402aa8f6661e60fd82a28746ad6d156f6c5336a70a39bd7b162b9"}, - {file = "yarl-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c42998fd1cbeb53cd985bff0e4bc25fbe55fd6eb3a545a724c1012d69d5ec84"}, - {file = "yarl-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c7c30fb38c300fe8140df30a046a01769105e4cf4282567a29b5cdb635b66c4"}, - {file = "yarl-1.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e49e0fd86c295e743fd5be69b8b0712f70a686bc79a16e5268386c2defacaade"}, - {file = "yarl-1.16.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:b9ca7b9147eb1365c8bab03c003baa1300599575effad765e0b07dd3501ea9af"}, - {file = "yarl-1.16.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:27e11db3f1e6a51081a981509f75617b09810529de508a181319193d320bc5c7"}, - {file = "yarl-1.16.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8994c42f4ca25df5380ddf59f315c518c81df6a68fed5bb0c159c6cb6b92f120"}, - {file = "yarl-1.16.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:542fa8e09a581bcdcbb30607c7224beff3fdfb598c798ccd28a8184ffc18b7eb"}, - {file = "yarl-1.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2bd6a51010c7284d191b79d3b56e51a87d8e1c03b0902362945f15c3d50ed46b"}, - {file = "yarl-1.16.0-cp310-cp310-win32.whl", hash = "sha256:178ccb856e265174a79f59721031060f885aca428983e75c06f78aa24b91d929"}, - {file = "yarl-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe8bba2545427418efc1929c5c42852bdb4143eb8d0a46b09de88d1fe99258e7"}, - {file = "yarl-1.16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d8643975a0080f361639787415a038bfc32d29208a4bf6b783ab3075a20b1ef3"}, - {file = "yarl-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:676d96bafc8c2d0039cea0cd3fd44cee7aa88b8185551a2bb93354668e8315c2"}, - {file = "yarl-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d9525f03269e64310416dbe6c68d3b23e5d34aaa8f47193a1c45ac568cecbc49"}, - {file = "yarl-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b37d5ec034e668b22cf0ce1074d6c21fd2a08b90d11b1b73139b750a8b0dd97"}, - {file = "yarl-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f32c4cb7386b41936894685f6e093c8dfaf0960124d91fe0ec29fe439e201d0"}, - {file = "yarl-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b8e265a0545637492a7e12fd7038370d66c9375a61d88c5567d0e044ded9202"}, - {file = "yarl-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:789a3423f28a5fff46fbd04e339863c169ece97c827b44de16e1a7a42bc915d2"}, - {file = "yarl-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1d1f45e3e8d37c804dca99ab3cf4ab3ed2e7a62cd82542924b14c0a4f46d243"}, - {file = "yarl-1.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:621280719c4c5dad4c1391160a9b88925bb8b0ff6a7d5af3224643024871675f"}, - {file = "yarl-1.16.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ed097b26f18a1f5ff05f661dc36528c5f6735ba4ce8c9645e83b064665131349"}, - {file = "yarl-1.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2f1fe2b2e3ee418862f5ebc0c0083c97f6f6625781382f828f6d4e9b614eba9b"}, - {file = "yarl-1.16.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:87dd10bc0618991c66cee0cc65fa74a45f4ecb13bceec3c62d78ad2e42b27a16"}, - {file = "yarl-1.16.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4199db024b58a8abb2cfcedac7b1292c3ad421684571aeb622a02f242280e8d6"}, - {file = "yarl-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:99a9dcd4b71dd5f5f949737ab3f356cfc058c709b4f49833aeffedc2652dac56"}, - {file = "yarl-1.16.0-cp311-cp311-win32.whl", hash = "sha256:a9394c65ae0ed95679717d391c862dece9afacd8fa311683fc8b4362ce8a410c"}, - {file = "yarl-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:5b9101f528ae0f8f65ac9d64dda2bb0627de8a50344b2f582779f32fda747c1d"}, - {file = "yarl-1.16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4ffb7c129707dd76ced0a4a4128ff452cecf0b0e929f2668ea05a371d9e5c104"}, - {file = "yarl-1.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1a5e9d8ce1185723419c487758d81ac2bde693711947032cce600ca7c9cda7d6"}, - {file = "yarl-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d743e3118b2640cef7768ea955378c3536482d95550222f908f392167fe62059"}, - {file = "yarl-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26768342f256e6e3c37533bf9433f5f15f3e59e3c14b2409098291b3efaceacb"}, - {file = "yarl-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1b0796168b953bca6600c5f97f5ed407479889a36ad7d17183366260f29a6b9"}, - {file = "yarl-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:858728086914f3a407aa7979cab743bbda1fe2bdf39ffcd991469a370dd7414d"}, - {file = "yarl-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5570e6d47bcb03215baf4c9ad7bf7c013e56285d9d35013541f9ac2b372593e7"}, - {file = "yarl-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66ea8311422a7ba1fc79b4c42c2baa10566469fe5a78500d4e7754d6e6db8724"}, - {file = "yarl-1.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:649bddcedee692ee8a9b7b6e38582cb4062dc4253de9711568e5620d8707c2a3"}, - {file = "yarl-1.16.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3a91654adb7643cb21b46f04244c5a315a440dcad63213033826549fa2435f71"}, - {file = "yarl-1.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b439cae82034ade094526a8f692b9a2b5ee936452de5e4c5f0f6c48df23f8604"}, - {file = "yarl-1.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:571f781ae8ac463ce30bacebfaef2c6581543776d5970b2372fbe31d7bf31a07"}, - {file = "yarl-1.16.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:aa7943f04f36d6cafc0cf53ea89824ac2c37acbdb4b316a654176ab8ffd0f968"}, - {file = "yarl-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1a5cf32539373ff39d97723e39a9283a7277cbf1224f7aef0c56c9598b6486c3"}, - {file = "yarl-1.16.0-cp312-cp312-win32.whl", hash = "sha256:a5b6c09b9b4253d6a208b0f4a2f9206e511ec68dce9198e0fbec4f160137aa67"}, - {file = "yarl-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1208ca14eed2fda324042adf8d6c0adf4a31522fa95e0929027cd487875f0240"}, - {file = "yarl-1.16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5ace0177520bd4caa99295a9b6fb831d0e9a57d8e0501a22ffaa61b4c024283"}, - {file = "yarl-1.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7118bdb5e3ed81acaa2095cba7ec02a0fe74b52a16ab9f9ac8e28e53ee299732"}, - {file = "yarl-1.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38fec8a2a94c58bd47c9a50a45d321ab2285ad133adefbbadf3012c054b7e656"}, - {file = "yarl-1.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8791d66d81ee45866a7bb15a517b01a2bcf583a18ebf5d72a84e6064c417e64b"}, - {file = "yarl-1.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cf936ba67bc6c734f3aa1c01391da74ab7fc046a9f8bbfa230b8393b90cf472"}, - {file = "yarl-1.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1aab176dd55b59f77a63b27cffaca67d29987d91a5b615cbead41331e6b7428"}, - {file = "yarl-1.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:995d0759004c08abd5d1b81300a91d18c8577c6389300bed1c7c11675105a44d"}, - {file = "yarl-1.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1bc22e00edeb068f71967ab99081e9406cd56dbed864fc3a8259442999d71552"}, - {file = "yarl-1.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:35b4f7842154176523e0a63c9b871168c69b98065d05a4f637fce342a6a2693a"}, - {file = "yarl-1.16.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:7ace71c4b7a0c41f317ae24be62bb61e9d80838d38acb20e70697c625e71f120"}, - {file = "yarl-1.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8f639e3f5795a6568aa4f7d2ac6057c757dcd187593679f035adbf12b892bb00"}, - {file = "yarl-1.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e8be3aff14f0120ad049121322b107f8a759be76a6a62138322d4c8a337a9e2c"}, - {file = "yarl-1.16.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:122d8e7986043d0549e9eb23c7fd23be078be4b70c9eb42a20052b3d3149c6f2"}, - {file = "yarl-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0fd9c227990f609c165f56b46107d0bc34553fe0387818c42c02f77974402c36"}, - {file = "yarl-1.16.0-cp313-cp313-win32.whl", hash = "sha256:595ca5e943baed31d56b33b34736461a371c6ea0038d3baec399949dd628560b"}, - {file = "yarl-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:921b81b8d78f0e60242fb3db615ea3f368827a76af095d5a69f1c3366db3f596"}, - {file = "yarl-1.16.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ab2b2ac232110a1fdb0d3ffcd087783edd3d4a6ced432a1bf75caf7b7be70916"}, - {file = "yarl-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7f8713717a09acbfee7c47bfc5777e685539fefdd34fa72faf504c8be2f3df4e"}, - {file = "yarl-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cdcffe1dbcb4477d2b4202f63cd972d5baa155ff5a3d9e35801c46a415b7f71a"}, - {file = "yarl-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a91217208306d82357c67daeef5162a41a28c8352dab7e16daa82e3718852a7"}, - {file = "yarl-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ab3ed42c78275477ea8e917491365e9a9b69bb615cb46169020bd0aa5e2d6d3"}, - {file = "yarl-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:707ae579ccb3262dfaef093e202b4c3fb23c3810e8df544b1111bd2401fd7b09"}, - {file = "yarl-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad7a852d1cd0b8d8b37fc9d7f8581152add917a98cfe2ea6e241878795f917ae"}, - {file = "yarl-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3f1cc3d3d4dc574bebc9b387f6875e228ace5748a7c24f49d8f01ac1bc6c31b"}, - {file = "yarl-1.16.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5ff96da263740779b0893d02b718293cc03400c3a208fc8d8cd79d9b0993e532"}, - {file = "yarl-1.16.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:3d375a19ba2bfe320b6d873f3fb165313b002cef8b7cc0a368ad8b8a57453837"}, - {file = "yarl-1.16.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:62c7da0ad93a07da048b500514ca47b759459ec41924143e2ddb5d7e20fd3db5"}, - {file = "yarl-1.16.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:147b0fcd0ee33b4b5f6edfea80452d80e419e51b9a3f7a96ce98eaee145c1581"}, - {file = "yarl-1.16.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:504e1fe1cc4f170195320eb033d2b0ccf5c6114ce5bf2f617535c01699479bca"}, - {file = "yarl-1.16.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:bdcf667a5dec12a48f669e485d70c54189f0639c2157b538a4cffd24a853624f"}, - {file = "yarl-1.16.0-cp39-cp39-win32.whl", hash = "sha256:e9951afe6557c75a71045148890052cb942689ee4c9ec29f5436240e1fcc73b7"}, - {file = "yarl-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:7d7aaa8ff95d0840e289423e7dc35696c2b058d635f945bf05b5cd633146b027"}, - {file = "yarl-1.16.0-py3-none-any.whl", hash = "sha256:e6980a558d8461230c457218bd6c92dfc1d10205548215c2c21d79dc8d0a96f3"}, - {file = "yarl-1.16.0.tar.gz", hash = "sha256:b6f687ced5510a9a2474bbae96a4352e5ace5fa34dc44a217b0537fec1db00b4"}, + {file = "yarl-1.17.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2d8715edfe12eee6f27f32a3655f38d6c7410deb482158c0b7d4b7fad5d07628"}, + {file = "yarl-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1803bf2a7a782e02db746d8bd18f2384801bc1d108723840b25e065b116ad726"}, + {file = "yarl-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e66589110e20c2951221a938fa200c7aa134a8bdf4e4dc97e6b21539ff026d4"}, + {file = "yarl-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7069d411cfccf868e812497e0ec4acb7c7bf8d684e93caa6c872f1e6f5d1664d"}, + {file = "yarl-1.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cbf70ba16118db3e4b0da69dcde9d4d4095d383c32a15530564c283fa38a7c52"}, + {file = "yarl-1.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0bc53cc349675b32ead83339a8de79eaf13b88f2669c09d4962322bb0f064cbc"}, + {file = "yarl-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6aa18a402d1c80193ce97c8729871f17fd3e822037fbd7d9b719864018df746"}, + {file = "yarl-1.17.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d89c5bc701861cfab357aa0cd039bc905fe919997b8c312b4b0c358619c38d4d"}, + {file = "yarl-1.17.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b728bdf38ca58f2da1d583e4af4ba7d4cd1a58b31a363a3137a8159395e7ecc7"}, + {file = "yarl-1.17.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:5542e57dc15d5473da5a39fbde14684b0cc4301412ee53cbab677925e8497c11"}, + {file = "yarl-1.17.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e564b57e5009fb150cb513804d7e9e9912fee2e48835638f4f47977f88b4a39c"}, + {file = "yarl-1.17.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:eb3c4cff524b4c1c1dba3a6da905edb1dfd2baf6f55f18a58914bbb2d26b59e1"}, + {file = "yarl-1.17.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:05e13f389038842da930d439fbed63bdce3f7644902714cb68cf527c971af804"}, + {file = "yarl-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:153c38ee2b4abba136385af4467459c62d50f2a3f4bde38c7b99d43a20c143ef"}, + {file = "yarl-1.17.0-cp310-cp310-win32.whl", hash = "sha256:4065b4259d1ae6f70fd9708ffd61e1c9c27516f5b4fae273c41028afcbe3a094"}, + {file = "yarl-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:abf366391a02a8335c5c26163b5fe6f514cc1d79e74d8bf3ffab13572282368e"}, + {file = "yarl-1.17.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:19a4fe0279626c6295c5b0c8c2bb7228319d2e985883621a6e87b344062d8135"}, + {file = "yarl-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cadd0113f4db3c6b56868d6a19ca6286f5ccfa7bc08c27982cf92e5ed31b489a"}, + {file = "yarl-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:60d6693eef43215b1ccfb1df3f6eae8db30a9ff1e7989fb6b2a6f0b468930ee8"}, + {file = "yarl-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb8bf3843e1fa8cf3fe77813c512818e57368afab7ebe9ef02446fe1a10b492"}, + {file = "yarl-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2a5b35fd1d8d90443e061d0c8669ac7600eec5c14c4a51f619e9e105b136715"}, + {file = "yarl-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c5bf17b32f392df20ab5c3a69d37b26d10efaa018b4f4e5643c7520d8eee7ac7"}, + {file = "yarl-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48f51b529b958cd06e78158ff297a8bf57b4021243c179ee03695b5dbf9cb6e1"}, + {file = "yarl-1.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fcaa06bf788e19f913d315d9c99a69e196a40277dc2c23741a1d08c93f4d430"}, + {file = "yarl-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:32f3ee19ff0f18a7a522d44e869e1ebc8218ad3ae4ebb7020445f59b4bbe5897"}, + {file = "yarl-1.17.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:a4fb69a81ae2ec2b609574ae35420cf5647d227e4d0475c16aa861dd24e840b0"}, + {file = "yarl-1.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7bacc8b77670322132a1b2522c50a1f62991e2f95591977455fd9a398b4e678d"}, + {file = "yarl-1.17.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:437bf6eb47a2d20baaf7f6739895cb049e56896a5ffdea61a4b25da781966e8b"}, + {file = "yarl-1.17.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:30534a03c87484092080e3b6e789140bd277e40f453358900ad1f0f2e61fc8ec"}, + {file = "yarl-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b30df4ff98703649915144be6f0df3b16fd4870ac38a09c56d5d9e54ff2d5f96"}, + {file = "yarl-1.17.0-cp311-cp311-win32.whl", hash = "sha256:263b487246858e874ab53e148e2a9a0de8465341b607678106829a81d81418c6"}, + {file = "yarl-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:07055a9e8b647a362e7d4810fe99d8f98421575e7d2eede32e008c89a65a17bd"}, + {file = "yarl-1.17.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:84095ab25ba69a8fa3fb4936e14df631b8a71193fe18bd38be7ecbe34d0f5512"}, + {file = "yarl-1.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02608fb3f6df87039212fc746017455ccc2a5fc96555ee247c45d1e9f21f1d7b"}, + {file = "yarl-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13468d291fe8c12162b7cf2cdb406fe85881c53c9e03053ecb8c5d3523822cd9"}, + {file = "yarl-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8da3f8f368fb7e2f052fded06d5672260c50b5472c956a5f1bd7bf474ae504ab"}, + {file = "yarl-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec0507ab6523980bed050137007c76883d941b519aca0e26d4c1ec1f297dd646"}, + {file = "yarl-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08fc76df7fd8360e9ff30e6ccc3ee85b8dbd6ed5d3a295e6ec62bcae7601b932"}, + {file = "yarl-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d522f390686acb6bab2b917dd9ca06740c5080cd2eaa5aef8827b97e967319d"}, + {file = "yarl-1.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:147c527a80bb45b3dcd6e63401af8ac574125d8d120e6afe9901049286ff64ef"}, + {file = "yarl-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:24cf43bcd17a0a1f72284e47774f9c60e0bf0d2484d5851f4ddf24ded49f33c6"}, + {file = "yarl-1.17.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c28a44b9e0fba49c3857360e7ad1473fc18bc7f6659ca08ed4f4f2b9a52c75fa"}, + {file = "yarl-1.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:350cacb2d589bc07d230eb995d88fcc646caad50a71ed2d86df533a465a4e6e1"}, + {file = "yarl-1.17.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:fd1ab1373274dea1c6448aee420d7b38af163b5c4732057cd7ee9f5454efc8b1"}, + {file = "yarl-1.17.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4934e0f96dadc567edc76d9c08181633c89c908ab5a3b8f698560124167d9488"}, + {file = "yarl-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8d0a278170d75c88e435a1ce76557af6758bfebc338435b2eba959df2552163e"}, + {file = "yarl-1.17.0-cp312-cp312-win32.whl", hash = "sha256:61584f33196575a08785bb56db6b453682c88f009cd9c6f338a10f6737ce419f"}, + {file = "yarl-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:9987a439ad33a7712bd5bbd073f09ad10d38640425fa498ecc99d8aa064f8fc4"}, + {file = "yarl-1.17.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8deda7b8eb15a52db94c2014acdc7bdd14cb59ec4b82ac65d2ad16dc234a109e"}, + {file = "yarl-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56294218b348dcbd3d7fce0ffd79dd0b6c356cb2a813a1181af730b7c40de9e7"}, + {file = "yarl-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1fab91292f51c884b290ebec0b309a64a5318860ccda0c4940e740425a67b6b7"}, + {file = "yarl-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cf93fa61ff4d9c7d40482ce1a2c9916ca435e34a1b8451e17f295781ccc034f"}, + {file = "yarl-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:261be774a0d71908c8830c33bacc89eef15c198433a8cc73767c10eeeb35a7d0"}, + {file = "yarl-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deec9693b67f6af856a733b8a3e465553ef09e5e8ead792f52c25b699b8f9e6e"}, + {file = "yarl-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c804b07622ba50a765ca7fb8145512836ab65956de01307541def869e4a456c9"}, + {file = "yarl-1.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d013a7c9574e98c14831a8f22d27277688ec3b2741d0188ac01a910b009987a"}, + {file = "yarl-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e2cfcba719bd494c7413dcf0caafb51772dec168c7c946e094f710d6aa70494e"}, + {file = "yarl-1.17.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:c068aba9fc5b94dfae8ea1cedcbf3041cd4c64644021362ffb750f79837e881f"}, + {file = "yarl-1.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3616df510ffac0df3c9fa851a40b76087c6c89cbcea2de33a835fc80f9faac24"}, + {file = "yarl-1.17.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:755d6176b442fba9928a4df787591a6a3d62d4969f05c406cad83d296c5d4e05"}, + {file = "yarl-1.17.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c18f6e708d1cf9ff5b1af026e697ac73bea9cb70ee26a2b045b112548579bed2"}, + {file = "yarl-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5b937c216b6dee8b858c6afea958de03c5ff28406257d22b55c24962a2baf6fd"}, + {file = "yarl-1.17.0-cp313-cp313-win32.whl", hash = "sha256:d0131b14cb545c1a7bd98f4565a3e9bdf25a1bd65c83fc156ee5d8a8499ec4a3"}, + {file = "yarl-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:01c96efa4313c01329e88b7e9e9e1b2fc671580270ddefdd41129fa8d0db7696"}, + {file = "yarl-1.17.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0d44f67e193f0a7acdf552ecb4d1956a3a276c68e7952471add9f93093d1c30d"}, + {file = "yarl-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:16ea0aa5f890cdcb7ae700dffa0397ed6c280840f637cd07bffcbe4b8d68b985"}, + {file = "yarl-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cf5469dc7dcfa65edf5cc3a6add9f84c5529c6b556729b098e81a09a92e60e51"}, + {file = "yarl-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e662bf2f6e90b73cf2095f844e2bc1fda39826472a2aa1959258c3f2a8500a2f"}, + {file = "yarl-1.17.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8260e88f1446904ba20b558fa8ce5d0ab9102747238e82343e46d056d7304d7e"}, + {file = "yarl-1.17.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dc16477a4a2c71e64c5d3d15d7ae3d3a6bb1e8b955288a9f73c60d2a391282f"}, + {file = "yarl-1.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46027e326cecd55e5950184ec9d86c803f4f6fe4ba6af9944a0e537d643cdbe0"}, + {file = "yarl-1.17.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc95e46c92a2b6f22e70afe07e34dbc03a4acd07d820204a6938798b16f4014f"}, + {file = "yarl-1.17.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:16ca76c7ac9515320cd09d6cc083d8d13d1803f6ebe212b06ea2505fd66ecff8"}, + {file = "yarl-1.17.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:eb1a5b97388f2613f9305d78a3473cdf8d80c7034e554d8199d96dcf80c62ac4"}, + {file = "yarl-1.17.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:41fd5498975418cdc34944060b8fbeec0d48b2741068077222564bea68daf5a6"}, + {file = "yarl-1.17.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:146ca582ed04a5664ad04b0e0603934281eaab5c0115a5a46cce0b3c061a56a1"}, + {file = "yarl-1.17.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:6abb8c06107dbec97481b2392dafc41aac091a5d162edf6ed7d624fe7da0587a"}, + {file = "yarl-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4d14be4613dd4f96c25feb4bd8c0d8ce0f529ab0ae555a17df5789e69d8ec0c5"}, + {file = "yarl-1.17.0-cp39-cp39-win32.whl", hash = "sha256:174d6a6cad1068f7850702aad0c7b1bca03bcac199ca6026f84531335dfc2646"}, + {file = "yarl-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:6af417ca2c7349b101d3fd557ad96b4cd439fdb6ab0d288e3f64a068eea394d0"}, + {file = "yarl-1.17.0-py3-none-any.whl", hash = "sha256:62dd42bb0e49423f4dd58836a04fcf09c80237836796025211bbe913f1524993"}, + {file = "yarl-1.17.0.tar.gz", hash = "sha256:d3f13583f378930377e02002b4085a3d025b00402d5a80911726d43a67911cd9"}, ] [package.dependencies] @@ -5017,4 +5284,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.0" python-versions = "<3.11.9,>=3.10" -content-hash = "b7c2e9664977bb7296d12399542edd5cb05f2b16c82d6e2d86c751abf4217e7f" +content-hash = "6735216f471aa3495775ae1196534c06539e0e5f17c60c15cf72b64c37fddc9c" diff --git a/pyproject.toml b/pyproject.toml index 7b6d139..191765c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ jsonschema = "<4.4.0,>=4.3.0" pandas = ">=1.3.0" pyalgotrade = "==0.20" lyra-v2-client = ">=0.2.9" -balpy = "0.0.7" +open-balpy = "0.0.7" numpy = "==1.26.1" dateparser = ">=1.1.1" open-multicaller = "==0.2.0" diff --git a/tox.ini b/tox.ini index b3ce566..52b456a 100644 --- a/tox.ini +++ b/tox.ini @@ -38,8 +38,19 @@ deps = pytest==7.2.1 openapi-core==0.15.0 openapi-spec-validator<0.5.0,>=0.4.0 + pyalgotrade==0.20 + pydantic==2.9.2 + pandas>=1.3.0 + solana==0.30.2 + open-aea-ledger-solana==1.57.0 + dateparser>=1.1.1 + lyra-v2-client>=0.2.9 open-multicaller==0.2.0 - balpy==0.0.7 + open-balpy==0.0.7 + pyyaml<=6.0.1,>=3.10 + dateparser>=1.1.1 + numpy==1.26.1 + [extra-deps] deps = From 5215ad1f6a9d16b702384846655064a1620a5f8a Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Tue, 29 Oct 2024 17:15:02 +0530 Subject: [PATCH 29/41] chore: Update tox.ini to handle unknown packages --- tox.ini | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 52b456a..5e3343d 100644 --- a/tox.ini +++ b/tox.ini @@ -567,6 +567,7 @@ unauthorized_licenses: GPLv3+ GNU General Public License v3 (GPLv3) + [Authorized Packages] gym: >=0.15 ;filelock is public domain @@ -582,4 +583,14 @@ paramiko: >=3.1.0 ; sub-dep of docker-compose websocket-client: >=0.59.0 pathable: ==0.4.3 -aiohappyeyeballs: >=2.3.4 \ No newline at end of file +aiohappyeyeballs: >=2.3.4 +pillow: >=6 +PyAlgoTrade: ==0.20 +lyra-v2-client: >=0.2.10 +open-multicaller==0.2.0 +open-balpy==0.0.7 + +anchorpy: >=0.17.0 +based58: >=0.1.1,<0.2.0 +jsonalias: ==0.1.1 +jsonrpcclient: ==4.0.3 \ No newline at end of file From 7249b9a6311b2d95b308d3ec3f8bbb11a811f8e8 Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Tue, 29 Oct 2024 18:26:33 +0530 Subject: [PATCH 30/41] chore: Update dependencies & fsm specs --- .../valory/agents/optimus/aea-config.yaml | 20 +-- packages/valory/services/optimus/service.yaml | 4 +- .../fsm_specification.yaml | 21 ++++ .../skills/liquidity_trader_abci/rounds.py | 7 +- .../skills/liquidity_trader_abci/skill.yaml | 8 +- .../optimus_abci/fsm_specification.yaml | 114 +++++++++++++++++- .../valory/skills/optimus_abci/skill.yaml | 12 +- pyproject.toml | 4 +- tox.ini | 8 +- 9 files changed, 163 insertions(+), 35 deletions(-) diff --git a/packages/valory/agents/optimus/aea-config.yaml b/packages/valory/agents/optimus/aea-config.yaml index 7aaaf68..717ff85 100644 --- a/packages/valory/agents/optimus/aea-config.yaml +++ b/packages/valory/agents/optimus/aea-config.yaml @@ -8,7 +8,7 @@ fingerprint: __init__.py: bafybeigx5mdvnamsqfum5ut7htok2y5vsnu7lrvms5gfvqi7hmv7sfbo3a fingerprint_ignore_patterns: [] connections: -- eightballer/dcxt:0.1.0:bafybeihnaxpiki57ivphf24qpie5g6rxwnztbrymutlw4yuyjxzvj3pdry +- eightballer/dcxt:0.1.0:bafybeiexzq2nvwi5mpgsnbfjorfd4hr5v56ocy4xshjyqcv43nwcbo6zk4 - valory/abci:0.1.0:bafybeiejymu4ul62zx6weoibnlsrfprfpjnplhjefz6sr6izgdr4sajlnu - valory/http_client:0.23.0:bafybeihi772xgzpqeipp3fhmvpct4y6e6tpjp4sogwqrnf3wqspgeilg4u - valory/http_server:0.22.0:bafybeihpgu56ovmq4npazdbh6y6ru5i7zuv6wvdglpxavsckyih56smu7m @@ -37,17 +37,17 @@ protocols: skills: - valory/abstract_abci:0.1.0:bafybeidz54kvxhbdmpruzguuzzq7bjg4pekjb5amqobkxoy4oqknnobopu - valory/abstract_round_abci:0.1.0:bafybeiajjzuh6vf23crp55humonknirvv2f4s3dmdlfzch6tc5ow52pcgm -- valory/liquidity_trader_abci:0.1.0:bafybeib2r7z5ufnb5nmtvob4bejb6ql3rrocqb6oinsze7xcaa33ulibye -- valory/market_data_fetcher_abci:0.1.0:bafybeibaf2ubj56busl7ngaamyjtm3tqsxthluuve6znxeogoxo7ta6b3y -- valory/strategy_evaluator_abci:0.1.0:bafybeibsk56r3dd5tza5yl2ltbonxdkb7zgfrupgiq3iyzr2ywqycskvz4 -- valory/optimus_abci:0.1.0:bafybeider6qgvmw64zzigonfp6coaap3gubredzqcz7oq7luf3aqkqincm +- valory/liquidity_trader_abci:0.1.0:bafybeigbpqzyvlc4xqkepq7ph5cebp2itr2bn2f4stiv3wpuko5hzzgmre +- valory/market_data_fetcher_abci:0.1.0:bafybeies3ib3ft3tqepogstxjarq3ku4xepnfozyvg2rur5ztq4afchgcq +- valory/strategy_evaluator_abci:0.1.0:bafybeidbib2sdjgq3354emzcc22zzgdm4z5hjdz2gbvcqabw5j4rdgcx5a +- valory/optimus_abci:0.1.0:bafybeifdbwlnkhlho22tudcini32gc64kcujisoib3yqlbs6uextv6uwsu - valory/registration_abci:0.1.0:bafybeiffipsowrqrkhjoexem7ern5ob4fabgif7wa6gtlszcoaop2e3oey - valory/reset_pause_abci:0.1.0:bafybeif4lgvbzsmzljesxbphycdv52ka7qnihyjrjpfaseclxadcmm6yiq - valory/termination_abci:0.1.0:bafybeiekkpo5qef5zaeagm3si6v45qxcojvtjqe4a5ceccvk4q7k3xi3bi - valory/transaction_settlement_abci:0.1.0:bafybeielv6eivt2z6nforq43xewl2vmpfwpdu2s2vfogobziljnwsclmlm - valory/trader_decision_maker_abci:0.1.0:bafybeicskzcqr7dd4d5ulwss5u3hsx2wvcxatugtxiuja2neobbr3tqtkm - valory/ipfs_package_downloader:0.1.0:bafybeieokinjosnulrsee3kbj7ly4kjfx2ub6lmwkyplgs33vxgmx3fbvm -- valory/portfolio_tracker_abci:0.1.0:bafybeidprqeiomlvs6e5nvcqr2hnntwiuwyaxifidznjowoexx65yoplmq +- valory/portfolio_tracker_abci:0.1.0:bafybeihigssk7zxefcr44hpxwrnsa6ho53zyulqhepxvm3idsywzd6w5wq default_ledger: ethereum required_ledgers: - ethereum @@ -152,7 +152,7 @@ type: skill models: benchmark_tool: args: - log_dir: ${str:/logs} + log_dir: ${str:/Users/gauravlochab/repos/optimus/logs} get_balance: args: api_id: ${str:get_balance} @@ -272,7 +272,7 @@ models: termination_from_block: ${int:34088325} allowed_dexs: ${list:["balancerPool", "UniswapV3"]} initial_assets: ${str:{"ethereum":{"0x0000000000000000000000000000000000000000":"ETH","0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48":"USDC"}}} - safe_contract_addresses: ${str:{"ethereum":"0x0000000000000000000000000000000000000000","base":"0x07e27E181Df065141ee90a4DD43cE4113bc9853C","optimism":"0x07e27E181Df065141ee90a4DD43cE4113bc9853C"}} + safe_contract_addresses: ${str:{"ethereum":"0x553Ce54DE9b219ecFfa9B65AEF49597c884AC64a","optimism":"0x07e27E181Df065141ee90a4DD43cE4113bc9853C","base":"0x07e27E181Df065141ee90a4DD43cE4113bc9853C"}} merkl_fetch_campaigns_args: ${str:{"url":"https://api.merkl.xyz/v3/campaigns","creator":"","live":"true"}} allowed_chains: ${list:["optimism","base"]} gas_reserve: ${str:{"ethereum":1000,"optimism":1000,"base":1000}} @@ -298,12 +298,12 @@ models: tenderly_access_key: ${str:access_key} tenderly_account_slug: ${str:account_slug} tenderly_project_slug: ${str:project_slug} - agent_transition: ${bool:true} + agent_transition: ${bool:True} chain_to_chain_id_mapping: ${str:{"optimism":10,"base":8453,"ethereum":1}} staking_token_contract_address: ${str:0x88996bbdE7f982D93214881756840cE2c77C4992} staking_activity_checker_contract_address: ${str:0x7Fd1F4b764fA41d19fe3f63C85d12bf64d2bbf68} staking_threshold_period: ${int:5} - store_path: ${str:/data/} + store_path: ${str:/Users/gauravlochab/repos/optimus/data/} assets_info_filename: ${str:assets.json} pool_info_filename: ${str:current_pool.json} gas_cost_info_filename: ${str:gas_costs.json} diff --git a/packages/valory/services/optimus/service.yaml b/packages/valory/services/optimus/service.yaml index 49c0cfd..d9869b8 100644 --- a/packages/valory/services/optimus/service.yaml +++ b/packages/valory/services/optimus/service.yaml @@ -6,7 +6,7 @@ aea_version: '>=1.0.0, <2.0.0' license: Apache-2.0 fingerprint: {} fingerprint_ignore_patterns: [] -agent: valory/optimus:0.1.0:bafybeida2scmw3qune3n6ru7tuzquuc3mxs2cfivzcncrtlj4ziadv4sqy +agent: valory/optimus:0.1.0:bafybeig2jtohkfll7szfr2cij6alvly3wrjr75t2mgfnqd6aylir4rc2qm number_of_agents: 1 deployment: {} --- @@ -128,4 +128,4 @@ cert_requests: not_after: '2023-01-01' not_before: '2022-01-01' public_key: ${ACN_NODE_PUBLIC_KEY:str:02d3a830c9d6ea1ae91936951430dee11f4662f33118b02190693be835359a9d77} - save_path: .certs/acn_cosmos_11000.txt \ No newline at end of file + save_path: .certs/acn_cosmos_11000.txt diff --git a/packages/valory/skills/liquidity_trader_abci/fsm_specification.yaml b/packages/valory/skills/liquidity_trader_abci/fsm_specification.yaml index cb93863..473b5c5 100644 --- a/packages/valory/skills/liquidity_trader_abci/fsm_specification.yaml +++ b/packages/valory/skills/liquidity_trader_abci/fsm_specification.yaml @@ -2,8 +2,11 @@ alphabet_in: - ACTION_EXECUTED - CHECKPOINT_TX_EXECUTED - DONE +- DONT_MOVE_TO_NEXT_AGENT - ERROR +- MOVE_TO_NEXT_AGENT - NEXT_CHECKPOINT_NOT_REACHED_YET +- NONE - NO_MAJORITY - ROUND_TIMEOUT - SERVICE_EVICTED @@ -23,16 +26,22 @@ final_states: - FinishedDecisionMakingRound - FinishedEvaluateStrategyRound - FinishedTxPreparationRound +- SwitchAgentEndingRound +- SwitchAgentStartingRound label: LiquidityTraderAbciApp start_states: - CallCheckpointRound - CheckStakingKPIMetRound +- DecideAgentEndingRound +- DecideAgentStartingRound - DecisionMakingRound - GetPositionsRound - PostTxSettlementRound states: - CallCheckpointRound - CheckStakingKPIMetRound +- DecideAgentEndingRound +- DecideAgentStartingRound - DecisionMakingRound - EvaluateStrategyRound - FailedMultiplexerRound @@ -43,6 +52,8 @@ states: - FinishedTxPreparationRound - GetPositionsRound - PostTxSettlementRound +- SwitchAgentEndingRound +- SwitchAgentStartingRound transition_func: (CallCheckpointRound, DONE): CheckStakingKPIMetRound (CallCheckpointRound, NEXT_CHECKPOINT_NOT_REACHED_YET): CheckStakingKPIMetRound @@ -58,6 +69,16 @@ transition_func: (CheckStakingKPIMetRound, SETTLE): FinishedCheckStakingKPIMetRound (CheckStakingKPIMetRound, STAKING_KPI_MET): GetPositionsRound (CheckStakingKPIMetRound, STAKING_KPI_NOT_MET): GetPositionsRound + (DecideAgentEndingRound, DONE): PostTxSettlementRound + (DecideAgentEndingRound, DONT_MOVE_TO_NEXT_AGENT): PostTxSettlementRound + (DecideAgentEndingRound, MOVE_TO_NEXT_AGENT): SwitchAgentEndingRound + (DecideAgentEndingRound, NONE): PostTxSettlementRound + (DecideAgentEndingRound, NO_MAJORITY): PostTxSettlementRound + (DecideAgentStartingRound, DONE): CallCheckpointRound + (DecideAgentStartingRound, DONT_MOVE_TO_NEXT_AGENT): CallCheckpointRound + (DecideAgentStartingRound, MOVE_TO_NEXT_AGENT): SwitchAgentStartingRound + (DecideAgentStartingRound, NONE): CallCheckpointRound + (DecideAgentStartingRound, NO_MAJORITY): CallCheckpointRound (DecisionMakingRound, DONE): FinishedDecisionMakingRound (DecisionMakingRound, ERROR): FinishedDecisionMakingRound (DecisionMakingRound, NO_MAJORITY): DecisionMakingRound diff --git a/packages/valory/skills/liquidity_trader_abci/rounds.py b/packages/valory/skills/liquidity_trader_abci/rounds.py index 3d228e1..95dc781 100644 --- a/packages/valory/skills/liquidity_trader_abci/rounds.py +++ b/packages/valory/skills/liquidity_trader_abci/rounds.py @@ -406,8 +406,6 @@ class DecideAgentStartingRound(VotingRound): payload_class = DecideAgentStartingPayload synchronized_data_class = SynchronizedData done_event = Event.DONE - negative_event = Event.NEGATIVE - none_event = Event.NONE no_majority_event = Event.NO_MAJORITY collection_key = get_name(SynchronizedData.participant_to_votes) @@ -437,8 +435,6 @@ class DecideAgentEndingRound(VotingRound): payload_class = DecideAgentEndingPayload synchronized_data_class = SynchronizedData done_event = Event.DONE - negative_event = Event.NEGATIVE - none_event = Event.NONE no_majority_event = Event.NO_MAJORITY collection_key = get_name(SynchronizedData.participant_to_votes) @@ -577,6 +573,7 @@ class LiquidityTraderAbciApp(AbciApp[Event]): Event.DONT_MOVE_TO_NEXT_AGENT: PostTxSettlementRound, Event.MOVE_TO_NEXT_AGENT: SwitchAgentEndingRound, Event.DONE: PostTxSettlementRound, + Event.NONE: PostTxSettlementRound, Event.NO_MAJORITY: PostTxSettlementRound, }, PostTxSettlementRound: { @@ -607,6 +604,8 @@ class LiquidityTraderAbciApp(AbciApp[Event]): } event_to_timeout: Dict[Event, float] = { Event.ROUND_TIMEOUT: 30.0, + Event.NONE: 30.0, + } cross_period_persisted_keys: FrozenSet[str] = frozenset( { diff --git a/packages/valory/skills/liquidity_trader_abci/skill.yaml b/packages/valory/skills/liquidity_trader_abci/skill.yaml index ebd7db7..a09484e 100644 --- a/packages/valory/skills/liquidity_trader_abci/skill.yaml +++ b/packages/valory/skills/liquidity_trader_abci/skill.yaml @@ -7,16 +7,16 @@ license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeia7bn2ahqqwkf63ptje6rfnftuwrsp33sswgpcbh5osbesxxr6g4m - behaviours.py: bafybeibkyfgwxu5bqw33vny3loat3liqe74emq6d5uxxnwzobx3kvydaca + behaviours.py: bafybeick3pktxnsjc3kfcaez3gukpll4cf5l6seqe4g64jef4p6yqvwiqe dialogues.py: bafybeiay23otskx2go5xhtgdwfw2kd6rxd62sxxdu3njv7hageorl5zxzm - fsm_specification.yaml: bafybeiabbiulb7k6xkjysulmy6o4ugnhxlpp5jiaeextvwj65q4ttadoeq + fsm_specification.yaml: bafybeibrbx2numojewsyokyx4jqprvuikvo2kgcrqvixgzlynf3dipxxzm handlers.py: bafybeidxw2lvgiifmo4siobpwuwbxscuifrdo3gnkjyn6bgexotj5f7zf4 models.py: bafybeihhdgk5ui4qmcszamyaxthtgizrlmmbmlo2y6hu7taqis5u44favq - payloads.py: bafybeidrarae35mhurj3d6hgeznqpa2a522x42rjmcfbhnrhyoe2rayxgy + payloads.py: bafybeighy3zb4yzbr4ognejz7pnv7xldlumgcsbbsn776nne4uzov3ef4i pool_behaviour.py: bafybeiaheuesscgqzwjbpyrezgwpdbdfurlmfwbc462qv6rblwwxlx5dpm pools/balancer.py: bafybeigznhgv7ylo5dvlhxcqikhiuqlqtnx3ikv4tszyvkl2lpcuqgoa5u pools/uniswap.py: bafybeigmqptgmjaxscszohfusgxsexqyx4awuyw7p4g5l7k2qpeyq7vdcu - rounds.py: bafybeiajgtt32rl32mgtd6t6qjxrx4xcsemfmsryemrdadzvow4cyjgcsy + rounds.py: bafybeif3y7tyt73rkzlh2srnxbxj4jycylmw3s45uoupbdda2f2ifgrmhu strategies/simple_strategy.py: bafybeiasu2nchowx6leksjllpuum4ckezxoj4o2m4sstavblplvvutmvzm strategy_behaviour.py: bafybeidk6sorg47kuuubamcccksi65x3txldyo7y2hm5opbye2ghmz2ljy fingerprint_ignore_patterns: [] diff --git a/packages/valory/skills/optimus_abci/fsm_specification.yaml b/packages/valory/skills/optimus_abci/fsm_specification.yaml index 81b20ff..22c4b7a 100644 --- a/packages/valory/skills/optimus_abci/fsm_specification.yaml +++ b/packages/valory/skills/optimus_abci/fsm_specification.yaml @@ -1,19 +1,40 @@ alphabet_in: - ACTION_EXECUTED +- BACKTEST_FAILED +- BACKTEST_NEGATIVE +- BACKTEST_POSITIVE +- BACKTEST_POSITIVE_EVM +- BACKTEST_POSITIVE_PROXY_SERVER - CHECKPOINT_TX_EXECUTED - CHECK_HISTORY - CHECK_LATE_ARRIVING_MESSAGE - CHECK_TIMEOUT - DONE +- DONT_MOVE_TO_NEXT_AGENT - ERROR +- ERROR_BACKTESTING +- ERROR_PREPARING_INSTRUCTIONS +- ERROR_PREPARING_SWAPS +- FAILED - FINALIZATION_FAILED - FINALIZE_TIMEOUT +- INCOMPLETE_INSTRUCTIONS_PREPARED - INCORRECT_SERIALIZATION +- INSTRUCTIONS_PREPARED +- INSUFFICIENT_BALANCE - INSUFFICIENT_FUNDS +- MOVE_TO_NEXT_AGENT - NEGATIVE - NEXT_CHECKPOINT_NOT_REACHED_YET - NONE +- NO_INSTRUCTIONS - NO_MAJORITY +- NO_ORDERS +- PREPARE_INCOMPLETE_SWAP +- PREPARE_SWAP +- PROXY_SWAPPED +- PROXY_SWAP_FAILED +- PROXY_SWAP_TIMEOUT - RESET_AND_PAUSE_TIMEOUT - RESET_TIMEOUT - ROUND_TIMEOUT @@ -23,6 +44,10 @@ alphabet_in: - STAKING_KPI_MET - STAKING_KPI_NOT_MET - SUSPICIOUS_ACTIVITY +- SWAPS_QUEUE_EMPTY +- SWAP_TX_PREPARED +- TRANSACTION_PREPARED +- TX_PREPARATION_FAILED - UNRECOGNIZED - UPDATE - VALIDATE_TIMEOUT @@ -35,16 +60,25 @@ start_states: - RegistrationRound - RegistrationStartupRound states: +- BacktestRound - CallCheckpointRound - CheckLateTxHashesRound - CheckStakingKPIMetRound - CheckTransactionHistoryRound - CollectSignatureRound +- DecideAgentEndingRound +- DecideAgentStartingRound - DecisionMakingRound - EvaluateStrategyRound +- FetchMarketDataRound - FinalizationRound - GetPositionsRound +- PortfolioTrackerRound - PostTxSettlementRound +- PrepareEvmSwapRound +- PrepareSwapRound +- ProxySwapQueueRound +- RandomnessRound - RandomnessTransactionSubmissionRound - RegistrationRound - RegistrationStartupRound @@ -53,9 +87,21 @@ states: - SelectKeeperTransactionSubmissionARound - SelectKeeperTransactionSubmissionBAfterTimeoutRound - SelectKeeperTransactionSubmissionBRound +- StrategyExecRound +- SwapQueueRound - SynchronizeLateMessagesRound +- TraderDecisionMakerRound +- TransformMarketDataRound - ValidateTransactionRound transition_func: + (BacktestRound, BACKTEST_FAILED): RandomnessRound + (BacktestRound, BACKTEST_NEGATIVE): RandomnessRound + (BacktestRound, BACKTEST_POSITIVE): PrepareSwapRound + (BacktestRound, BACKTEST_POSITIVE_EVM): PrepareEvmSwapRound + (BacktestRound, BACKTEST_POSITIVE_PROXY_SERVER): ProxySwapQueueRound + (BacktestRound, ERROR_BACKTESTING): RandomnessRound + (BacktestRound, NO_MAJORITY): BacktestRound + (BacktestRound, ROUND_TIMEOUT): BacktestRound (CallCheckpointRound, DONE): CheckStakingKPIMetRound (CallCheckpointRound, NEXT_CHECKPOINT_NOT_REACHED_YET): CheckStakingKPIMetRound (CallCheckpointRound, NO_MAJORITY): CallCheckpointRound @@ -65,7 +111,7 @@ transition_func: (CallCheckpointRound, SETTLE): RandomnessTransactionSubmissionRound (CheckLateTxHashesRound, CHECK_LATE_ARRIVING_MESSAGE): SynchronizeLateMessagesRound (CheckLateTxHashesRound, CHECK_TIMEOUT): CheckLateTxHashesRound - (CheckLateTxHashesRound, DONE): PostTxSettlementRound + (CheckLateTxHashesRound, DONE): DecideAgentEndingRound (CheckLateTxHashesRound, NEGATIVE): ResetAndPauseRound (CheckLateTxHashesRound, NONE): ResetAndPauseRound (CheckLateTxHashesRound, NO_MAJORITY): ResetAndPauseRound @@ -78,13 +124,23 @@ transition_func: (CheckStakingKPIMetRound, STAKING_KPI_NOT_MET): GetPositionsRound (CheckTransactionHistoryRound, CHECK_LATE_ARRIVING_MESSAGE): SynchronizeLateMessagesRound (CheckTransactionHistoryRound, CHECK_TIMEOUT): CheckTransactionHistoryRound - (CheckTransactionHistoryRound, DONE): PostTxSettlementRound + (CheckTransactionHistoryRound, DONE): DecideAgentEndingRound (CheckTransactionHistoryRound, NEGATIVE): SelectKeeperTransactionSubmissionBRound (CheckTransactionHistoryRound, NONE): ResetAndPauseRound (CheckTransactionHistoryRound, NO_MAJORITY): CheckTransactionHistoryRound (CollectSignatureRound, DONE): FinalizationRound (CollectSignatureRound, NO_MAJORITY): ResetRound (CollectSignatureRound, ROUND_TIMEOUT): CollectSignatureRound + (DecideAgentEndingRound, DONE): PostTxSettlementRound + (DecideAgentEndingRound, DONT_MOVE_TO_NEXT_AGENT): PostTxSettlementRound + (DecideAgentEndingRound, MOVE_TO_NEXT_AGENT): RandomnessRound + (DecideAgentEndingRound, NONE): PostTxSettlementRound + (DecideAgentEndingRound, NO_MAJORITY): PostTxSettlementRound + (DecideAgentStartingRound, DONE): CallCheckpointRound + (DecideAgentStartingRound, DONT_MOVE_TO_NEXT_AGENT): CallCheckpointRound + (DecideAgentStartingRound, MOVE_TO_NEXT_AGENT): RandomnessRound + (DecideAgentStartingRound, NONE): CallCheckpointRound + (DecideAgentStartingRound, NO_MAJORITY): CallCheckpointRound (DecisionMakingRound, DONE): ResetAndPauseRound (DecisionMakingRound, ERROR): ResetAndPauseRound (DecisionMakingRound, NO_MAJORITY): DecisionMakingRound @@ -95,6 +151,10 @@ transition_func: (EvaluateStrategyRound, NO_MAJORITY): EvaluateStrategyRound (EvaluateStrategyRound, ROUND_TIMEOUT): EvaluateStrategyRound (EvaluateStrategyRound, WAIT): ResetAndPauseRound + (FetchMarketDataRound, DONE): TransformMarketDataRound + (FetchMarketDataRound, NONE): RandomnessRound + (FetchMarketDataRound, NO_MAJORITY): FetchMarketDataRound + (FetchMarketDataRound, ROUND_TIMEOUT): FetchMarketDataRound (FinalizationRound, CHECK_HISTORY): CheckTransactionHistoryRound (FinalizationRound, CHECK_LATE_ARRIVING_MESSAGE): SynchronizeLateMessagesRound (FinalizationRound, DONE): ValidateTransactionRound @@ -104,18 +164,41 @@ transition_func: (GetPositionsRound, DONE): EvaluateStrategyRound (GetPositionsRound, NO_MAJORITY): GetPositionsRound (GetPositionsRound, ROUND_TIMEOUT): GetPositionsRound + (PortfolioTrackerRound, DONE): StrategyExecRound + (PortfolioTrackerRound, FAILED): RandomnessRound + (PortfolioTrackerRound, INSUFFICIENT_BALANCE): PortfolioTrackerRound + (PortfolioTrackerRound, NO_MAJORITY): PortfolioTrackerRound + (PortfolioTrackerRound, ROUND_TIMEOUT): PortfolioTrackerRound (PostTxSettlementRound, ACTION_EXECUTED): DecisionMakingRound (PostTxSettlementRound, CHECKPOINT_TX_EXECUTED): CallCheckpointRound (PostTxSettlementRound, ROUND_TIMEOUT): PostTxSettlementRound (PostTxSettlementRound, UNRECOGNIZED): ResetAndPauseRound (PostTxSettlementRound, VANITY_TX_EXECUTED): CheckStakingKPIMetRound + (PrepareEvmSwapRound, NO_INSTRUCTIONS): PrepareEvmSwapRound + (PrepareEvmSwapRound, NO_MAJORITY): PrepareEvmSwapRound + (PrepareEvmSwapRound, ROUND_TIMEOUT): PrepareEvmSwapRound + (PrepareEvmSwapRound, TRANSACTION_PREPARED): RandomnessTransactionSubmissionRound + (PrepareSwapRound, ERROR_PREPARING_INSTRUCTIONS): RandomnessRound + (PrepareSwapRound, INCOMPLETE_INSTRUCTIONS_PREPARED): SwapQueueRound + (PrepareSwapRound, INSTRUCTIONS_PREPARED): SwapQueueRound + (PrepareSwapRound, NO_INSTRUCTIONS): ResetAndPauseRound + (PrepareSwapRound, NO_MAJORITY): PrepareSwapRound + (PrepareSwapRound, ROUND_TIMEOUT): PrepareSwapRound + (ProxySwapQueueRound, NO_MAJORITY): ProxySwapQueueRound + (ProxySwapQueueRound, PROXY_SWAPPED): ProxySwapQueueRound + (ProxySwapQueueRound, PROXY_SWAP_FAILED): ProxySwapQueueRound + (ProxySwapQueueRound, PROXY_SWAP_TIMEOUT): ProxySwapQueueRound + (ProxySwapQueueRound, SWAPS_QUEUE_EMPTY): ResetAndPauseRound + (RandomnessRound, DONE): TraderDecisionMakerRound + (RandomnessRound, NO_MAJORITY): RandomnessRound + (RandomnessRound, ROUND_TIMEOUT): RandomnessRound (RandomnessTransactionSubmissionRound, DONE): SelectKeeperTransactionSubmissionARound (RandomnessTransactionSubmissionRound, NO_MAJORITY): RandomnessTransactionSubmissionRound (RandomnessTransactionSubmissionRound, ROUND_TIMEOUT): RandomnessTransactionSubmissionRound - (RegistrationRound, DONE): CallCheckpointRound + (RegistrationRound, DONE): DecideAgentStartingRound (RegistrationRound, NO_MAJORITY): RegistrationRound - (RegistrationStartupRound, DONE): CallCheckpointRound - (ResetAndPauseRound, DONE): CallCheckpointRound + (RegistrationStartupRound, DONE): DecideAgentStartingRound + (ResetAndPauseRound, DONE): DecideAgentStartingRound (ResetAndPauseRound, NO_MAJORITY): RegistrationRound (ResetAndPauseRound, RESET_AND_PAUSE_TIMEOUT): RegistrationRound (ResetRound, DONE): RandomnessTransactionSubmissionRound @@ -135,11 +218,30 @@ transition_func: (SelectKeeperTransactionSubmissionBRound, INCORRECT_SERIALIZATION): ResetAndPauseRound (SelectKeeperTransactionSubmissionBRound, NO_MAJORITY): ResetRound (SelectKeeperTransactionSubmissionBRound, ROUND_TIMEOUT): SelectKeeperTransactionSubmissionBRound + (StrategyExecRound, ERROR_PREPARING_SWAPS): RandomnessRound + (StrategyExecRound, NO_MAJORITY): StrategyExecRound + (StrategyExecRound, NO_ORDERS): ResetAndPauseRound + (StrategyExecRound, PREPARE_INCOMPLETE_SWAP): BacktestRound + (StrategyExecRound, PREPARE_SWAP): BacktestRound + (StrategyExecRound, ROUND_TIMEOUT): StrategyExecRound + (SwapQueueRound, NO_MAJORITY): SwapQueueRound + (SwapQueueRound, ROUND_TIMEOUT): SwapQueueRound + (SwapQueueRound, SWAPS_QUEUE_EMPTY): ResetAndPauseRound + (SwapQueueRound, SWAP_TX_PREPARED): RandomnessTransactionSubmissionRound + (SwapQueueRound, TX_PREPARATION_FAILED): SwapQueueRound (SynchronizeLateMessagesRound, DONE): CheckLateTxHashesRound (SynchronizeLateMessagesRound, NONE): SelectKeeperTransactionSubmissionBRound (SynchronizeLateMessagesRound, ROUND_TIMEOUT): SynchronizeLateMessagesRound (SynchronizeLateMessagesRound, SUSPICIOUS_ACTIVITY): ResetAndPauseRound - (ValidateTransactionRound, DONE): PostTxSettlementRound + (TraderDecisionMakerRound, DONE): FetchMarketDataRound + (TraderDecisionMakerRound, NONE): RandomnessRound + (TraderDecisionMakerRound, NO_MAJORITY): RandomnessRound + (TraderDecisionMakerRound, ROUND_TIMEOUT): RandomnessRound + (TransformMarketDataRound, DONE): PortfolioTrackerRound + (TransformMarketDataRound, NONE): RandomnessRound + (TransformMarketDataRound, NO_MAJORITY): TransformMarketDataRound + (TransformMarketDataRound, ROUND_TIMEOUT): TransformMarketDataRound + (ValidateTransactionRound, DONE): DecideAgentEndingRound (ValidateTransactionRound, NEGATIVE): CheckTransactionHistoryRound (ValidateTransactionRound, NONE): SelectKeeperTransactionSubmissionBRound (ValidateTransactionRound, NO_MAJORITY): ValidateTransactionRound diff --git a/packages/valory/skills/optimus_abci/skill.yaml b/packages/valory/skills/optimus_abci/skill.yaml index c39ca31..3905665 100644 --- a/packages/valory/skills/optimus_abci/skill.yaml +++ b/packages/valory/skills/optimus_abci/skill.yaml @@ -10,9 +10,9 @@ fingerprint: behaviours.py: bafybeidutyxryf6h7ooc7ggrdl5lojfv3anrvf3zq4tbwkiwxm7qqrriye composition.py: bafybeiczokwfi2h6qkw4lk5cp52tafddxxysh6qwrnzqt4cfu67yb5a2nq dialogues.py: bafybeift7d4mk4qkzm4rqmdrfv43erhunzyqmwn6pecuumq25amy7q7fp4 - fsm_specification.yaml: bafybeiehe6ps7xi7i7tu4mduid7vbvszhsiu5juyard24i3nhtqgljpcza + fsm_specification.yaml: bafybeicyobvq7eow6dabjk6sd7yyqh2agbrumkuiljz2hc2ouz3by7tjui handlers.py: bafybeibjxph27kmpljyeqkednkrfgrtelksfs3kbgjbvh6eycjrfqz5o2y - models.py: bafybeibbnyxant6quqcbzwebu5jyws46mjpvpvsxnr3yrnq6slhrbd6zsa + models.py: bafybeicgyhodahs7r5nizhto3omlbu3vd2ryehgbfmocy6d4sgi3rddqgy fingerprint_ignore_patterns: [] connections: [] contracts: [] @@ -25,12 +25,12 @@ skills: - valory/registration_abci:0.1.0:bafybeiffipsowrqrkhjoexem7ern5ob4fabgif7wa6gtlszcoaop2e3oey - valory/reset_pause_abci:0.1.0:bafybeif4lgvbzsmzljesxbphycdv52ka7qnihyjrjpfaseclxadcmm6yiq - valory/termination_abci:0.1.0:bafybeiekkpo5qef5zaeagm3si6v45qxcojvtjqe4a5ceccvk4q7k3xi3bi -- valory/liquidity_trader_abci:0.1.0:bafybeib2r7z5ufnb5nmtvob4bejb6ql3rrocqb6oinsze7xcaa33ulibye +- valory/liquidity_trader_abci:0.1.0:bafybeigbpqzyvlc4xqkepq7ph5cebp2itr2bn2f4stiv3wpuko5hzzgmre - valory/transaction_settlement_abci:0.1.0:bafybeielv6eivt2z6nforq43xewl2vmpfwpdu2s2vfogobziljnwsclmlm -- valory/market_data_fetcher_abci:0.1.0:bafybeibaf2ubj56busl7ngaamyjtm3tqsxthluuve6znxeogoxo7ta6b3y +- valory/market_data_fetcher_abci:0.1.0:bafybeies3ib3ft3tqepogstxjarq3ku4xepnfozyvg2rur5ztq4afchgcq - valory/trader_decision_maker_abci:0.1.0:bafybeicskzcqr7dd4d5ulwss5u3hsx2wvcxatugtxiuja2neobbr3tqtkm -- valory/strategy_evaluator_abci:0.1.0:bafybeibsk56r3dd5tza5yl2ltbonxdkb7zgfrupgiq3iyzr2ywqycskvz4 -- valory/portfolio_tracker_abci:0.1.0:bafybeidprqeiomlvs6e5nvcqr2hnntwiuwyaxifidznjowoexx65yoplmq +- valory/strategy_evaluator_abci:0.1.0:bafybeidbib2sdjgq3354emzcc22zzgdm4z5hjdz2gbvcqabw5j4rdgcx5a +- valory/portfolio_tracker_abci:0.1.0:bafybeihigssk7zxefcr44hpxwrnsa6ho53zyulqhepxvm3idsywzd6w5wq behaviours: main: args: {} diff --git a/pyproject.toml b/pyproject.toml index 191765c..95f550e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ web3 = "<7,>=6.0.0" ipfshttpclient = "==0.8.0a2" open-aea-cli-ipfs = "==1.57.0" aiohttp = "<4.0.0,>=3.8.5" -certifi = "*" +certifi = "==2021.10.8" multidict = "*" ecdsa = ">=0.15" eth_typing = "*" @@ -60,7 +60,7 @@ jsonschema = "<4.4.0,>=4.3.0" pandas = ">=1.3.0" pyalgotrade = "==0.20" lyra-v2-client = ">=0.2.9" -open-balpy = "0.0.7" +open-balpy = "==0.0.7" numpy = "==1.26.1" dateparser = ">=1.1.1" open-multicaller = "==0.2.0" diff --git a/tox.ini b/tox.ini index 5e3343d..ffbb64c 100644 --- a/tox.ini +++ b/tox.ini @@ -54,6 +54,7 @@ deps = [extra-deps] deps = + PyYAML>=5.4.1 attrs black==24.2.0 ecdsa>=0.15 @@ -62,16 +63,21 @@ deps = eth_abi==4.0.0 eth_typing eth_utils - hexbytes + hexbytes==0.3.1 ipfshttpclient==0.8.0a2 isort==5.13.2 multidict packaging protobuf<4.25.0,>=4.21.6 + py-eth-sig-utils + py-multibase==1.0.3 + py-multicodec==0.2.1 pycryptodome==3.18.0 pytest-asyncio requests<2.31.2,>=2.28.1 + urllib3==1.26.16 web3<7,>=6.0.0 + websocket_client<1,>=0.32.0 werkzeug ; end-extra From b7a6969a0e6df1a2f174154d271e9340d4095b4c Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Tue, 29 Oct 2024 18:43:44 +0530 Subject: [PATCH 31/41] chore: Update dependencies --- packages/packages.json | 8 ++++---- packages/valory/agents/optimus/aea-config.yaml | 8 ++++---- packages/valory/services/optimus/service.yaml | 2 +- packages/valory/skills/liquidity_trader_abci/skill.yaml | 4 ++-- packages/valory/skills/optimus_abci/skill.yaml | 6 +++--- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/packages.json b/packages/packages.json index c05bdd4..415826f 100644 --- a/packages/packages.json +++ b/packages/packages.json @@ -8,10 +8,10 @@ "contract/valory/merkl_distributor/0.1.0": "bafybeihaqsvmncuzmwv2r6iuzc5t7ur6ugdhephz7ydftypksjidpsylbq", "contract/valory/staking_token/0.1.0": "bafybeifrvtkofw5c26b3irm6izqfdpik6vpjhm6hqwcdzx333h6vhdanai", "contract/valory/staking_activity_checker/0.1.0": "bafybeibjzsi2r5b6xd4iwl4wbwldptnynryzsdpifym4mkv32ynswx22ou", - "skill/valory/liquidity_trader_abci/0.1.0": "bafybeib2r7z5ufnb5nmtvob4bejb6ql3rrocqb6oinsze7xcaa33ulibye", - "skill/valory/optimus_abci/0.1.0": "bafybeib5ctllbumgne5j6xaenkss7lwmeyn5bkcrftap5cyf6gdwbo4rhq", - "agent/valory/optimus/0.1.0": "bafybeiggdox3555xcvvzzlrls74sjzrz4imh3ki4srpw6emgusvqumf3bm", - "service/valory/optimus/0.1.0": "bafybeifmgufluiixnao3hnf7uz5bgi2qvhr4m6mbul2gghjghveagffemi" + "skill/valory/liquidity_trader_abci/0.1.0": "bafybeibxgayrhcuc6r63fjvo2zvuurby4d3weasoccyjad5c4el5logcfq", + "skill/valory/optimus_abci/0.1.0": "bafybeiddf5rudq2kdlzmy5oq3ukhqzl2sfkzv456uybzzpkbecf73cy2hu", + "agent/valory/optimus/0.1.0": "bafybeidsaqz3zc7g7mjfmw5p3zydg4aru546re4yce3ddpyiibocvwvnt4", + "service/valory/optimus/0.1.0": "bafybeihvqep3iyzu7x4ihtcqqbonkgzejulttefw2yvppwzmxxdb7y67la" }, "third_party": { "custom/eightballer/rsi_strategy/0.1.0": "bafybeigbofp2nqwcxu3rlkuugpc3w6ils3u7glse7c335rddcqg56ybh34", diff --git a/packages/valory/agents/optimus/aea-config.yaml b/packages/valory/agents/optimus/aea-config.yaml index 717ff85..70c5051 100644 --- a/packages/valory/agents/optimus/aea-config.yaml +++ b/packages/valory/agents/optimus/aea-config.yaml @@ -37,10 +37,10 @@ protocols: skills: - valory/abstract_abci:0.1.0:bafybeidz54kvxhbdmpruzguuzzq7bjg4pekjb5amqobkxoy4oqknnobopu - valory/abstract_round_abci:0.1.0:bafybeiajjzuh6vf23crp55humonknirvv2f4s3dmdlfzch6tc5ow52pcgm -- valory/liquidity_trader_abci:0.1.0:bafybeigbpqzyvlc4xqkepq7ph5cebp2itr2bn2f4stiv3wpuko5hzzgmre +- valory/liquidity_trader_abci:0.1.0:bafybeibxgayrhcuc6r63fjvo2zvuurby4d3weasoccyjad5c4el5logcfq - valory/market_data_fetcher_abci:0.1.0:bafybeies3ib3ft3tqepogstxjarq3ku4xepnfozyvg2rur5ztq4afchgcq - valory/strategy_evaluator_abci:0.1.0:bafybeidbib2sdjgq3354emzcc22zzgdm4z5hjdz2gbvcqabw5j4rdgcx5a -- valory/optimus_abci:0.1.0:bafybeifdbwlnkhlho22tudcini32gc64kcujisoib3yqlbs6uextv6uwsu +- valory/optimus_abci:0.1.0:bafybeiddf5rudq2kdlzmy5oq3ukhqzl2sfkzv456uybzzpkbecf73cy2hu - valory/registration_abci:0.1.0:bafybeiffipsowrqrkhjoexem7ern5ob4fabgif7wa6gtlszcoaop2e3oey - valory/reset_pause_abci:0.1.0:bafybeif4lgvbzsmzljesxbphycdv52ka7qnihyjrjpfaseclxadcmm6yiq - valory/termination_abci:0.1.0:bafybeiekkpo5qef5zaeagm3si6v45qxcojvtjqe4a5ceccvk4q7k3xi3bi @@ -152,7 +152,7 @@ type: skill models: benchmark_tool: args: - log_dir: ${str:/Users/gauravlochab/repos/optimus/logs} + log_dir: ${str:/logs} get_balance: args: api_id: ${str:get_balance} @@ -303,7 +303,7 @@ models: staking_token_contract_address: ${str:0x88996bbdE7f982D93214881756840cE2c77C4992} staking_activity_checker_contract_address: ${str:0x7Fd1F4b764fA41d19fe3f63C85d12bf64d2bbf68} staking_threshold_period: ${int:5} - store_path: ${str:/Users/gauravlochab/repos/optimus/data/} + store_path: ${str:/data/} assets_info_filename: ${str:assets.json} pool_info_filename: ${str:current_pool.json} gas_cost_info_filename: ${str:gas_costs.json} diff --git a/packages/valory/services/optimus/service.yaml b/packages/valory/services/optimus/service.yaml index d9869b8..c41edde 100644 --- a/packages/valory/services/optimus/service.yaml +++ b/packages/valory/services/optimus/service.yaml @@ -6,7 +6,7 @@ aea_version: '>=1.0.0, <2.0.0' license: Apache-2.0 fingerprint: {} fingerprint_ignore_patterns: [] -agent: valory/optimus:0.1.0:bafybeig2jtohkfll7szfr2cij6alvly3wrjr75t2mgfnqd6aylir4rc2qm +agent: valory/optimus:0.1.0:bafybeidsaqz3zc7g7mjfmw5p3zydg4aru546re4yce3ddpyiibocvwvnt4 number_of_agents: 1 deployment: {} --- diff --git a/packages/valory/skills/liquidity_trader_abci/skill.yaml b/packages/valory/skills/liquidity_trader_abci/skill.yaml index a09484e..8151146 100644 --- a/packages/valory/skills/liquidity_trader_abci/skill.yaml +++ b/packages/valory/skills/liquidity_trader_abci/skill.yaml @@ -9,14 +9,14 @@ fingerprint: __init__.py: bafybeia7bn2ahqqwkf63ptje6rfnftuwrsp33sswgpcbh5osbesxxr6g4m behaviours.py: bafybeick3pktxnsjc3kfcaez3gukpll4cf5l6seqe4g64jef4p6yqvwiqe dialogues.py: bafybeiay23otskx2go5xhtgdwfw2kd6rxd62sxxdu3njv7hageorl5zxzm - fsm_specification.yaml: bafybeibrbx2numojewsyokyx4jqprvuikvo2kgcrqvixgzlynf3dipxxzm + fsm_specification.yaml: bafybeiartxssodz3t2lthu7q7hc6r32agkrzbva56m2sozd7xiusvmbl4i handlers.py: bafybeidxw2lvgiifmo4siobpwuwbxscuifrdo3gnkjyn6bgexotj5f7zf4 models.py: bafybeihhdgk5ui4qmcszamyaxthtgizrlmmbmlo2y6hu7taqis5u44favq payloads.py: bafybeighy3zb4yzbr4ognejz7pnv7xldlumgcsbbsn776nne4uzov3ef4i pool_behaviour.py: bafybeiaheuesscgqzwjbpyrezgwpdbdfurlmfwbc462qv6rblwwxlx5dpm pools/balancer.py: bafybeigznhgv7ylo5dvlhxcqikhiuqlqtnx3ikv4tszyvkl2lpcuqgoa5u pools/uniswap.py: bafybeigmqptgmjaxscszohfusgxsexqyx4awuyw7p4g5l7k2qpeyq7vdcu - rounds.py: bafybeif3y7tyt73rkzlh2srnxbxj4jycylmw3s45uoupbdda2f2ifgrmhu + rounds.py: bafybeianuvn7ei647wxtttuynd4h3n3uj7pzp6o37hxsmjqav7uad4hszm strategies/simple_strategy.py: bafybeiasu2nchowx6leksjllpuum4ckezxoj4o2m4sstavblplvvutmvzm strategy_behaviour.py: bafybeidk6sorg47kuuubamcccksi65x3txldyo7y2hm5opbye2ghmz2ljy fingerprint_ignore_patterns: [] diff --git a/packages/valory/skills/optimus_abci/skill.yaml b/packages/valory/skills/optimus_abci/skill.yaml index 3905665..a02a9e4 100644 --- a/packages/valory/skills/optimus_abci/skill.yaml +++ b/packages/valory/skills/optimus_abci/skill.yaml @@ -10,9 +10,9 @@ fingerprint: behaviours.py: bafybeidutyxryf6h7ooc7ggrdl5lojfv3anrvf3zq4tbwkiwxm7qqrriye composition.py: bafybeiczokwfi2h6qkw4lk5cp52tafddxxysh6qwrnzqt4cfu67yb5a2nq dialogues.py: bafybeift7d4mk4qkzm4rqmdrfv43erhunzyqmwn6pecuumq25amy7q7fp4 - fsm_specification.yaml: bafybeicyobvq7eow6dabjk6sd7yyqh2agbrumkuiljz2hc2ouz3by7tjui + fsm_specification.yaml: bafybeiasafyyxit3jv5tpd73knlw245oge357yj3af6lzvowmu5gqemic4 handlers.py: bafybeibjxph27kmpljyeqkednkrfgrtelksfs3kbgjbvh6eycjrfqz5o2y - models.py: bafybeicgyhodahs7r5nizhto3omlbu3vd2ryehgbfmocy6d4sgi3rddqgy + models.py: bafybeibbnyxant6quqcbzwebu5jyws46mjpvpvsxnr3yrnq6slhrbd6zsa fingerprint_ignore_patterns: [] connections: [] contracts: [] @@ -25,7 +25,7 @@ skills: - valory/registration_abci:0.1.0:bafybeiffipsowrqrkhjoexem7ern5ob4fabgif7wa6gtlszcoaop2e3oey - valory/reset_pause_abci:0.1.0:bafybeif4lgvbzsmzljesxbphycdv52ka7qnihyjrjpfaseclxadcmm6yiq - valory/termination_abci:0.1.0:bafybeiekkpo5qef5zaeagm3si6v45qxcojvtjqe4a5ceccvk4q7k3xi3bi -- valory/liquidity_trader_abci:0.1.0:bafybeigbpqzyvlc4xqkepq7ph5cebp2itr2bn2f4stiv3wpuko5hzzgmre +- valory/liquidity_trader_abci:0.1.0:bafybeibxgayrhcuc6r63fjvo2zvuurby4d3weasoccyjad5c4el5logcfq - valory/transaction_settlement_abci:0.1.0:bafybeielv6eivt2z6nforq43xewl2vmpfwpdu2s2vfogobziljnwsclmlm - valory/market_data_fetcher_abci:0.1.0:bafybeies3ib3ft3tqepogstxjarq3ku4xepnfozyvg2rur5ztq4afchgcq - valory/trader_decision_maker_abci:0.1.0:bafybeicskzcqr7dd4d5ulwss5u3hsx2wvcxatugtxiuja2neobbr3tqtkm From 1e02d693ed403d9eda7722083a8ba91bb9f6f0d2 Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Tue, 29 Oct 2024 19:18:09 +0530 Subject: [PATCH 32/41] chore: resolving for linters --- .../liquidity_trader_abci/behaviours.py | 26 ++++---- .../skills/liquidity_trader_abci/models.py | 4 +- .../skills/liquidity_trader_abci/payloads.py | 7 ++- .../skills/liquidity_trader_abci/rounds.py | 34 ++++++----- .../valory/skills/optimus_abci/behaviours.py | 11 +--- .../valory/skills/optimus_abci/composition.py | 4 +- .../valory/skills/optimus_abci/dialogues.py | 3 +- .../valory/skills/optimus_abci/handlers.py | 1 + packages/valory/skills/optimus_abci/models.py | 59 +++++++++++-------- scripts/aea-config-replace.py | 4 +- 10 files changed, 86 insertions(+), 67 deletions(-) diff --git a/packages/valory/skills/liquidity_trader_abci/behaviours.py b/packages/valory/skills/liquidity_trader_abci/behaviours.py index 5f632e9..bd1e406 100644 --- a/packages/valory/skills/liquidity_trader_abci/behaviours.py +++ b/packages/valory/skills/liquidity_trader_abci/behaviours.py @@ -79,6 +79,10 @@ CallCheckpointRound, CheckStakingKPIMetPayload, CheckStakingKPIMetRound, + DecideAgentEndingPayload, + DecideAgentEndingRound, + DecideAgentStartingPayload, + DecideAgentStartingRound, DecisionMakingPayload, DecisionMakingRound, EvaluateStrategyPayload, @@ -91,10 +95,6 @@ PostTxSettlementRound, StakingState, SynchronizedData, - DecideAgentStartingRound, - DecideAgentStartingPayload, - DecideAgentEndingRound, - DecideAgentEndingPayload, ) from packages.valory.skills.liquidity_trader_abci.strategies.simple_strategy import ( SimpleStrategyBehaviour, @@ -3358,6 +3358,7 @@ def fetch_and_log_gas_details(self): "Gas used or effective gas price not found in the response." ) + class DecideAgentStartingBehaviour(LiquidityTraderBaseBehaviour): """Behaviour that executes all the actions.""" @@ -3366,13 +3367,16 @@ class DecideAgentStartingBehaviour(LiquidityTraderBaseBehaviour): def async_act(self) -> Generator: """Async act""" with self.context.benchmark_tool.measure(self.behaviour_id).local(): - payload = DecideAgentStartingPayload(self.context.agent_address, self.params.agent_transition) + payload = DecideAgentStartingPayload( + self.context.agent_address, self.params.agent_transition + ) with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): yield from self.send_a2a_transaction(payload) yield from self.wait_until_round_end() - self.set_done() + self.set_done() + class DecideAgentEndingBehaviour(LiquidityTraderBaseBehaviour): """Behaviour that executes all the actions.""" @@ -3382,13 +3386,16 @@ class DecideAgentEndingBehaviour(LiquidityTraderBaseBehaviour): def async_act(self) -> Generator: """Async act""" with self.context.benchmark_tool.measure(self.behaviour_id).local(): - payload = DecideAgentEndingPayload(self.context.agent_address, self.params.agent_transition) - + payload = DecideAgentEndingPayload( + self.context.agent_address, self.params.agent_transition + ) + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): yield from self.send_a2a_transaction(payload) yield from self.wait_until_round_end() - self.set_done() + self.set_done() + class LiquidityTraderRoundBehaviour(AbstractRoundBehaviour): """LiquidityTraderRoundBehaviour""" @@ -3405,4 +3412,3 @@ class LiquidityTraderRoundBehaviour(AbstractRoundBehaviour): DecideAgentEndingBehaviour, PostTxSettlementBehaviour, ] - diff --git a/packages/valory/skills/liquidity_trader_abci/models.py b/packages/valory/skills/liquidity_trader_abci/models.py index 880454d..b80728d 100644 --- a/packages/valory/skills/liquidity_trader_abci/models.py +++ b/packages/valory/skills/liquidity_trader_abci/models.py @@ -254,9 +254,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.min_swap_amount_threshold = self._ensure( "min_swap_amount_threshold", kwargs, int ) - self.agent_transition = self._ensure( - "agent_transition", kwargs, bool - ) + self.agent_transition = self._ensure("agent_transition", kwargs, bool) self.max_fee_percentage = self._ensure("max_fee_percentage", kwargs, float) self.max_gas_percentage = self._ensure("max_gas_percentage", kwargs, float) self.balancer_graphql_endpoints = json.loads( diff --git a/packages/valory/skills/liquidity_trader_abci/payloads.py b/packages/valory/skills/liquidity_trader_abci/payloads.py index 3bf6811..4a1f0f9 100644 --- a/packages/valory/skills/liquidity_trader_abci/payloads.py +++ b/packages/valory/skills/liquidity_trader_abci/payloads.py @@ -70,17 +70,20 @@ class DecisionMakingPayload(BaseTxPayload): content: str + @dataclass(frozen=True) class DecideAgentStartingPayload(BaseTxPayload): """Represent a transaction payload for the DecideAgentRound.""" - vote: bool + vote: bool + @dataclass(frozen=True) class DecideAgentEndingPayload(BaseTxPayload): """Represent a transaction payload for the DecideAgentRound.""" - vote: bool + vote: bool + @dataclass(frozen=True) class PostTxSettlementPayload(BaseTxPayload): diff --git a/packages/valory/skills/liquidity_trader_abci/rounds.py b/packages/valory/skills/liquidity_trader_abci/rounds.py index 95dc781..bb7ce8c 100644 --- a/packages/valory/skills/liquidity_trader_abci/rounds.py +++ b/packages/valory/skills/liquidity_trader_abci/rounds.py @@ -32,18 +32,18 @@ CollectionRound, DegenerateRound, DeserializedCollection, - get_name, VotingRound, + get_name, ) from packages.valory.skills.liquidity_trader_abci.payloads import ( CallCheckpointPayload, CheckStakingKPIMetPayload, + DecideAgentEndingPayload, + DecideAgentStartingPayload, DecisionMakingPayload, EvaluateStrategyPayload, GetPositionsPayload, PostTxSettlementPayload, - DecideAgentStartingPayload, - DecideAgentEndingPayload, ) @@ -57,6 +57,7 @@ class StakingState(Enum): class Event(Enum): """LiquidityTraderAbciApp Events""" + NEGATIVE = "negative" NONE = "none" ACTION_EXECUTED = "execute_next_action" @@ -76,7 +77,7 @@ class Event(Enum): STAKING_KPI_NOT_MET = "staking_kpi_not_met" STAKING_KPI_MET = "staking_kpi_met" MOVE_TO_NEXT_AGENT = "move_to_next_agent" - DONT_MOVE_TO_NEXT_AGENT = "dont_move_to_next_agent" # nosec + DONT_MOVE_TO_NEXT_AGENT = "dont_move_to_next_agent" # nosec class SynchronizedData(BaseSynchronizedData): @@ -400,6 +401,7 @@ def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: return self.synchronized_data, Event.NO_MAJORITY return None + class DecideAgentStartingRound(VotingRound): """DecisionMakingRound""" @@ -415,9 +417,8 @@ def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: # We reference all the events here to prevent the check-abciapp-specs tool from complaining synchronized_data = cast(SynchronizedData, self.synchronized_data) - return synchronized_data, Event.MOVE_TO_NEXT_AGENT - + if self.negative_vote_threshold_reached: return self.synchronized_data, Event.DONT_MOVE_TO_NEXT_AGENT if self.none_vote_threshold_reached: @@ -428,7 +429,7 @@ def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: return self.synchronized_data, self.no_majority_event return None - + class DecideAgentEndingRound(VotingRound): """DecideAgentRound""" @@ -444,7 +445,7 @@ def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: # We reference all the events here to prevent the check-abciapp-specs tool from complaining synchronized_data = cast(SynchronizedData, self.synchronized_data) return synchronized_data, Event.MOVE_TO_NEXT_AGENT - + if self.negative_vote_threshold_reached: return self.synchronized_data, Event.DONT_MOVE_TO_NEXT_AGENT if self.none_vote_threshold_reached: @@ -454,7 +455,8 @@ def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: ): return self.synchronized_data, self.no_majority_event return None - + + class PostTxSettlementRound(CollectSameUntilThresholdRound): """A round that will be called after tx settlement is done.""" @@ -503,13 +505,16 @@ class FinishedTxPreparationRound(DegenerateRound): class FailedMultiplexerRound(DegenerateRound): - """FailedMultiplexerRound""" + """FailedMultiplexerRound""" + class SwitchAgentStartingRound(DegenerateRound): - """SwitchAgentRound""" + """SwitchAgentRound""" + class SwitchAgentEndingRound(DegenerateRound): - """SwitchAgentRound""" + """SwitchAgentRound""" + class LiquidityTraderAbciApp(AbciApp[Event]): """LiquidityTraderAbciApp""" @@ -586,8 +591,8 @@ class LiquidityTraderAbciApp(AbciApp[Event]): FinishedEvaluateStrategyRound: {}, FinishedTxPreparationRound: {}, FinishedDecisionMakingRound: {}, - SwitchAgentStartingRound:{}, - SwitchAgentEndingRound:{}, + SwitchAgentStartingRound: {}, + SwitchAgentEndingRound: {}, FinishedCallCheckpointRound: {}, FinishedCheckStakingKPIMetRound: {}, FailedMultiplexerRound: {}, @@ -605,7 +610,6 @@ class LiquidityTraderAbciApp(AbciApp[Event]): event_to_timeout: Dict[Event, float] = { Event.ROUND_TIMEOUT: 30.0, Event.NONE: 30.0, - } cross_period_persisted_keys: FrozenSet[str] = frozenset( { diff --git a/packages/valory/skills/optimus_abci/behaviours.py b/packages/valory/skills/optimus_abci/behaviours.py index 3327eb0..c00c2cb 100644 --- a/packages/valory/skills/optimus_abci/behaviours.py +++ b/packages/valory/skills/optimus_abci/behaviours.py @@ -28,16 +28,13 @@ from packages.valory.skills.liquidity_trader_abci.behaviours import ( LiquidityTraderRoundBehaviour, ) - -from packages.valory.skills.portfolio_tracker_abci.behaviours import ( - PortfolioTrackerRoundBehaviour, -) - from packages.valory.skills.market_data_fetcher_abci.behaviours import ( MarketDataFetcherRoundBehaviour, ) - from packages.valory.skills.optimus_abci.composition import OptimusAbciApp +from packages.valory.skills.portfolio_tracker_abci.behaviours import ( + PortfolioTrackerRoundBehaviour, +) from packages.valory.skills.registration_abci.behaviours import ( AgentRegistrationRoundBehaviour, RegistrationStartupBehaviour, @@ -45,7 +42,6 @@ from packages.valory.skills.reset_pause_abci.behaviours import ( ResetPauseABCIConsensusBehaviour, ) - from packages.valory.skills.strategy_evaluator_abci.behaviours.round_behaviour import ( AgentStrategyEvaluatorRoundBehaviour, ) @@ -53,7 +49,6 @@ BackgroundBehaviour, TerminationAbciBehaviours, ) - from packages.valory.skills.trader_decision_maker_abci.behaviours import ( TraderDecisionMakerRoundBehaviour, ) diff --git a/packages/valory/skills/optimus_abci/composition.py b/packages/valory/skills/optimus_abci/composition.py index c68818f..7086017 100644 --- a/packages/valory/skills/optimus_abci/composition.py +++ b/packages/valory/skills/optimus_abci/composition.py @@ -43,7 +43,7 @@ RegistrationAbci.FinishedRegistrationRound: LiquidityTraderAbci.DecideAgentStartingRound, LiquidityTraderAbci.FinishedCallCheckpointRound: TransactionSettlementAbci.RandomnessTransactionSubmissionRound, LiquidityTraderAbci.FinishedCheckStakingKPIMetRound: TransactionSettlementAbci.RandomnessTransactionSubmissionRound, - LiquidityTraderAbci.SwitchAgentStartingRound:TraderDecisionMakerAbci.RandomnessRound, + LiquidityTraderAbci.SwitchAgentStartingRound: TraderDecisionMakerAbci.RandomnessRound, TraderDecisionMakerAbci.FinishedTraderDecisionMakerRound: MarketDataFetcherAbci.FetchMarketDataRound, TraderDecisionMakerAbci.FailedTraderDecisionMakerRound: TraderDecisionMakerAbci.RandomnessRound, MarketDataFetcherAbci.FinishedMarketFetchRound: PortfolioTrackerAbci.PortfolioTrackerRound, @@ -62,7 +62,7 @@ LiquidityTraderAbci.FinishedTxPreparationRound: TransactionSettlementAbci.RandomnessTransactionSubmissionRound, LiquidityTraderAbci.FailedMultiplexerRound: ResetAndPauseAbci.ResetAndPauseRound, TransactionSettlementAbci.FinishedTransactionSubmissionRound: LiquidityTraderAbci.DecideAgentEndingRound, - LiquidityTraderAbci.SwitchAgentEndingRound:TraderDecisionMakerAbci.RandomnessRound, + LiquidityTraderAbci.SwitchAgentEndingRound: TraderDecisionMakerAbci.RandomnessRound, TransactionSettlementAbci.FailedRound: ResetAndPauseAbci.ResetAndPauseRound, ResetAndPauseAbci.FinishedResetAndPauseRound: LiquidityTraderAbci.DecideAgentStartingRound, ResetAndPauseAbci.FinishedResetAndPauseErrorRound: RegistrationAbci.RegistrationRound, diff --git a/packages/valory/skills/optimus_abci/dialogues.py b/packages/valory/skills/optimus_abci/dialogues.py index 91f58b5..d50da2a 100644 --- a/packages/valory/skills/optimus_abci/dialogues.py +++ b/packages/valory/skills/optimus_abci/dialogues.py @@ -36,7 +36,6 @@ from packages.eightballer.protocols.tickers.dialogues import ( TickersDialogues as BaseTickersDialogues, ) - from packages.valory.skills.abstract_round_abci.dialogues import ( AbciDialogue as BaseAbciDialogue, ) @@ -114,4 +113,4 @@ BalancesDialogues = BaseBalancesDialogues OrdersDialogue = BaseOrdersDialogue -OrdersDialogues = BaseOrdersDialogues \ No newline at end of file +OrdersDialogues = BaseOrdersDialogues diff --git a/packages/valory/skills/optimus_abci/handlers.py b/packages/valory/skills/optimus_abci/handlers.py index 4d640e3..980265f 100644 --- a/packages/valory/skills/optimus_abci/handlers.py +++ b/packages/valory/skills/optimus_abci/handlers.py @@ -50,6 +50,7 @@ DcxtOrdersHandler as BaseDcxtOrdersHandler, ) + ABCIHandler = BaseABCIRoundHandler HttpHandler = BaseHttpHandler SigningHandler = BaseSigningHandler diff --git a/packages/valory/skills/optimus_abci/models.py b/packages/valory/skills/optimus_abci/models.py index 5c8d407..ef82d73 100644 --- a/packages/valory/skills/optimus_abci/models.py +++ b/packages/valory/skills/optimus_abci/models.py @@ -38,44 +38,43 @@ from packages.valory.skills.liquidity_trader_abci.rounds import ( Event as LiquidityTraderEvent, ) -from packages.valory.skills.optimus_abci.composition import OptimusAbciApp -from packages.valory.skills.reset_pause_abci.rounds import Event as ResetPauseEvent -from packages.valory.skills.termination_abci.models import TerminationParams -from packages.valory.skills.transaction_settlement_abci.rounds import ( - Event as TransactionSettlementEvent, +from packages.valory.skills.market_data_fetcher_abci.models import ( + Params as MarketDataFetcherParams, ) +from packages.valory.skills.market_data_fetcher_abci.rounds import ( + Event as MarketDataFetcherEvent, +) +from packages.valory.skills.optimus_abci.composition import OptimusAbciApp from packages.valory.skills.portfolio_tracker_abci.models import GetBalance +from packages.valory.skills.portfolio_tracker_abci.models import ( + Params as PortfolioTrackerParams, +) from packages.valory.skills.portfolio_tracker_abci.models import TokenAccounts +from packages.valory.skills.reset_pause_abci.rounds import Event as ResetPauseEvent +from packages.valory.skills.strategy_evaluator_abci.models import ( + StrategyEvaluatorParams as StrategyEvaluatorParams, +) from packages.valory.skills.strategy_evaluator_abci.models import ( SwapInstructionsSpecs, SwapQuotesSpecs, TxSettlementProxy, ) - -from packages.valory.skills.transaction_settlement_abci.models import TransactionParams -from packages.valory.skills.strategy_evaluator_abci.models import ( - StrategyEvaluatorParams as StrategyEvaluatorParams, -) -from packages.valory.skills.market_data_fetcher_abci.models import ( - Params as MarketDataFetcherParams, -) -from packages.valory.skills.portfolio_tracker_abci.models import ( - Params as PortfolioTrackerParams, +from packages.valory.skills.strategy_evaluator_abci.rounds import ( + Event as StrategyEvaluatorEvent, ) +from packages.valory.skills.termination_abci.models import TerminationParams from packages.valory.skills.trader_decision_maker_abci.models import ( Params as TraderDecisionMakerParams, ) - -from packages.valory.skills.market_data_fetcher_abci.rounds import ( - Event as MarketDataFetcherEvent, -) from packages.valory.skills.trader_decision_maker_abci.rounds import ( Event as DecisionMakingEvent, ) -from packages.valory.skills.strategy_evaluator_abci.rounds import ( - Event as StrategyEvaluatorEvent, +from packages.valory.skills.transaction_settlement_abci.models import TransactionParams +from packages.valory.skills.transaction_settlement_abci.rounds import ( + Event as TransactionSettlementEvent, ) + EventType = Union[ Type[LiquidityTraderEvent], Type[TransactionSettlementEvent], @@ -85,7 +84,14 @@ Type[StrategyEvaluatorEvent], ] EventToTimeoutMappingType = Dict[ - Union[LiquidityTraderEvent, TransactionSettlementEvent, ResetPauseEvent, MarketDataFetcherEvent, DecisionMakingEvent, StrategyEvaluatorEvent], + Union[ + LiquidityTraderEvent, + TransactionSettlementEvent, + ResetPauseEvent, + MarketDataFetcherEvent, + DecisionMakingEvent, + StrategyEvaluatorEvent, + ], float, ] @@ -130,7 +136,14 @@ def setup(self) -> None: """Set up.""" super().setup() - events = (LiquidityTraderEvent, TransactionSettlementEvent, ResetPauseEvent, MarketDataFetcherEvent, DecisionMakingEvent, StrategyEvaluatorEvent) + events = ( + LiquidityTraderEvent, + TransactionSettlementEvent, + ResetPauseEvent, + MarketDataFetcherEvent, + DecisionMakingEvent, + StrategyEvaluatorEvent, + ) round_timeout = self.params.round_timeout_seconds round_timeout_overrides = { cast(EventType, event).ROUND_TIMEOUT: round_timeout for event in events diff --git a/scripts/aea-config-replace.py b/scripts/aea-config-replace.py index 9ea2d77..7a903dc 100644 --- a/scripts/aea-config-replace.py +++ b/scripts/aea-config-replace.py @@ -32,7 +32,7 @@ def main() -> None: load_dotenv() with open(Path("optimus", "aea-config.yaml"), "r", encoding="utf-8") as file: - print("config path",Path("optimus", "aea-config.yaml")) + print("config path", Path("optimus", "aea-config.yaml")) config = list(yaml.safe_load_all(file)) # Ledger RPCs @@ -85,7 +85,7 @@ def main() -> None: "api_key" ] = f"${{str:{os.getenv('COINGECKO_API_KEY')}}}" except KeyError as e: - print("Error", e) + print("Error", e) config[5]["models"]["params"]["args"][ "allowed_chains" From a89cf25aa8c45df2c67e81073ec49784108755de Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Tue, 29 Oct 2024 19:19:44 +0530 Subject: [PATCH 33/41] chore: Update packages --- packages/packages.json | 8 ++++---- .../valory/agents/optimus/aea-config.yaml | 4 ++-- packages/valory/services/optimus/service.yaml | 20 ++++++++++++++++++- .../skills/liquidity_trader_abci/skill.yaml | 8 ++++---- .../valory/skills/optimus_abci/skill.yaml | 12 +++++------ 5 files changed, 35 insertions(+), 17 deletions(-) diff --git a/packages/packages.json b/packages/packages.json index 415826f..a38ffb5 100644 --- a/packages/packages.json +++ b/packages/packages.json @@ -8,10 +8,10 @@ "contract/valory/merkl_distributor/0.1.0": "bafybeihaqsvmncuzmwv2r6iuzc5t7ur6ugdhephz7ydftypksjidpsylbq", "contract/valory/staking_token/0.1.0": "bafybeifrvtkofw5c26b3irm6izqfdpik6vpjhm6hqwcdzx333h6vhdanai", "contract/valory/staking_activity_checker/0.1.0": "bafybeibjzsi2r5b6xd4iwl4wbwldptnynryzsdpifym4mkv32ynswx22ou", - "skill/valory/liquidity_trader_abci/0.1.0": "bafybeibxgayrhcuc6r63fjvo2zvuurby4d3weasoccyjad5c4el5logcfq", - "skill/valory/optimus_abci/0.1.0": "bafybeiddf5rudq2kdlzmy5oq3ukhqzl2sfkzv456uybzzpkbecf73cy2hu", - "agent/valory/optimus/0.1.0": "bafybeidsaqz3zc7g7mjfmw5p3zydg4aru546re4yce3ddpyiibocvwvnt4", - "service/valory/optimus/0.1.0": "bafybeihvqep3iyzu7x4ihtcqqbonkgzejulttefw2yvppwzmxxdb7y67la" + "skill/valory/liquidity_trader_abci/0.1.0": "bafybeid7yambarhmg5xnlwxvhqwmwjmvf3wr7a3zzzicmac7buroq5d4we", + "skill/valory/optimus_abci/0.1.0": "bafybeifjkfbj7symbsm73e3dxf6yv3nfxmaqkpi3szqexsv2zepeiea4fm", + "agent/valory/optimus/0.1.0": "bafybeie2eu7yzb6i4spwgq63zo6jdpbsxam5nlw4ridiatw5jwjjm4qxu4", + "service/valory/optimus/0.1.0": "bafybeib6fnkhtsayqps6vmknjlne6lnx7yb24wkrkijv6chcdm4lzalfc4" }, "third_party": { "custom/eightballer/rsi_strategy/0.1.0": "bafybeigbofp2nqwcxu3rlkuugpc3w6ils3u7glse7c335rddcqg56ybh34", diff --git a/packages/valory/agents/optimus/aea-config.yaml b/packages/valory/agents/optimus/aea-config.yaml index 70c5051..61d0202 100644 --- a/packages/valory/agents/optimus/aea-config.yaml +++ b/packages/valory/agents/optimus/aea-config.yaml @@ -37,10 +37,10 @@ protocols: skills: - valory/abstract_abci:0.1.0:bafybeidz54kvxhbdmpruzguuzzq7bjg4pekjb5amqobkxoy4oqknnobopu - valory/abstract_round_abci:0.1.0:bafybeiajjzuh6vf23crp55humonknirvv2f4s3dmdlfzch6tc5ow52pcgm -- valory/liquidity_trader_abci:0.1.0:bafybeibxgayrhcuc6r63fjvo2zvuurby4d3weasoccyjad5c4el5logcfq +- valory/liquidity_trader_abci:0.1.0:bafybeid7yambarhmg5xnlwxvhqwmwjmvf3wr7a3zzzicmac7buroq5d4we - valory/market_data_fetcher_abci:0.1.0:bafybeies3ib3ft3tqepogstxjarq3ku4xepnfozyvg2rur5ztq4afchgcq - valory/strategy_evaluator_abci:0.1.0:bafybeidbib2sdjgq3354emzcc22zzgdm4z5hjdz2gbvcqabw5j4rdgcx5a -- valory/optimus_abci:0.1.0:bafybeiddf5rudq2kdlzmy5oq3ukhqzl2sfkzv456uybzzpkbecf73cy2hu +- valory/optimus_abci:0.1.0:bafybeifjkfbj7symbsm73e3dxf6yv3nfxmaqkpi3szqexsv2zepeiea4fm - valory/registration_abci:0.1.0:bafybeiffipsowrqrkhjoexem7ern5ob4fabgif7wa6gtlszcoaop2e3oey - valory/reset_pause_abci:0.1.0:bafybeif4lgvbzsmzljesxbphycdv52ka7qnihyjrjpfaseclxadcmm6yiq - valory/termination_abci:0.1.0:bafybeiekkpo5qef5zaeagm3si6v45qxcojvtjqe4a5ceccvk4q7k3xi3bi diff --git a/packages/valory/services/optimus/service.yaml b/packages/valory/services/optimus/service.yaml index c41edde..20843b9 100644 --- a/packages/valory/services/optimus/service.yaml +++ b/packages/valory/services/optimus/service.yaml @@ -6,7 +6,7 @@ aea_version: '>=1.0.0, <2.0.0' license: Apache-2.0 fingerprint: {} fingerprint_ignore_patterns: [] -agent: valory/optimus:0.1.0:bafybeidsaqz3zc7g7mjfmw5p3zydg4aru546re4yce3ddpyiibocvwvnt4 +agent: valory/optimus:0.1.0:bafybeie2eu7yzb6i4spwgq63zo6jdpbsxam5nlw4ridiatw5jwjjm4qxu4 number_of_agents: 1 deployment: {} --- @@ -129,3 +129,21 @@ cert_requests: not_before: '2022-01-01' public_key: ${ACN_NODE_PUBLIC_KEY:str:02d3a830c9d6ea1ae91936951430dee11f4662f33118b02190693be835359a9d77} save_path: .certs/acn_cosmos_11000.txt +--- +public_id: valory/http_client:0.23.0 +type: connection +config: + host: ${HTTP_CLIENT_HOST:str:127.0.0.1} + port: ${HTTP_CLIENT_PORT:int:8000} + timeout: ${HTTP_CLIENT_TIMEOUT:int:1200} +--- +public_id: eightballer/dcxt:0.1.0 +type: connection +config: + target_skill_id: valory/trader_abci:0.1.0 + exchanges: + - name: ${DCXT_EXCHANGE_NAME:str:balancer} + key_path: ${DCXT_KEY_PATH:str:ethereum_private_key.txt} + ledger_id: ${DCXT_LEDGER_ID:str:ethereum} + rpc_url: ${DCXT_RPC_URL:str:http://host.docker.internal:8545} + etherscan_api_key: ${DCXT_ETHERSCAN_API_KEY:str:null} diff --git a/packages/valory/skills/liquidity_trader_abci/skill.yaml b/packages/valory/skills/liquidity_trader_abci/skill.yaml index 8151146..bf5492c 100644 --- a/packages/valory/skills/liquidity_trader_abci/skill.yaml +++ b/packages/valory/skills/liquidity_trader_abci/skill.yaml @@ -7,16 +7,16 @@ license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeia7bn2ahqqwkf63ptje6rfnftuwrsp33sswgpcbh5osbesxxr6g4m - behaviours.py: bafybeick3pktxnsjc3kfcaez3gukpll4cf5l6seqe4g64jef4p6yqvwiqe + behaviours.py: bafybeiaybs7vpbnsxhxztxi6u5m7tepfqviw4ttv4fzdzvc6avtb75rgoi dialogues.py: bafybeiay23otskx2go5xhtgdwfw2kd6rxd62sxxdu3njv7hageorl5zxzm fsm_specification.yaml: bafybeiartxssodz3t2lthu7q7hc6r32agkrzbva56m2sozd7xiusvmbl4i handlers.py: bafybeidxw2lvgiifmo4siobpwuwbxscuifrdo3gnkjyn6bgexotj5f7zf4 - models.py: bafybeihhdgk5ui4qmcszamyaxthtgizrlmmbmlo2y6hu7taqis5u44favq - payloads.py: bafybeighy3zb4yzbr4ognejz7pnv7xldlumgcsbbsn776nne4uzov3ef4i + models.py: bafybeieciktnznsw32woxt2z5oepka44lx7gfje3an6gevue4s6t7hghry + payloads.py: bafybeigpzmusgt7yp7skptv3gver53a42kqkndia2xtlyewvcwdafiwsyq pool_behaviour.py: bafybeiaheuesscgqzwjbpyrezgwpdbdfurlmfwbc462qv6rblwwxlx5dpm pools/balancer.py: bafybeigznhgv7ylo5dvlhxcqikhiuqlqtnx3ikv4tszyvkl2lpcuqgoa5u pools/uniswap.py: bafybeigmqptgmjaxscszohfusgxsexqyx4awuyw7p4g5l7k2qpeyq7vdcu - rounds.py: bafybeianuvn7ei647wxtttuynd4h3n3uj7pzp6o37hxsmjqav7uad4hszm + rounds.py: bafybeif6qpiswjrawbkvaqq5efxg6a4tdsluenyi5uakrrln7vzcgcq25m strategies/simple_strategy.py: bafybeiasu2nchowx6leksjllpuum4ckezxoj4o2m4sstavblplvvutmvzm strategy_behaviour.py: bafybeidk6sorg47kuuubamcccksi65x3txldyo7y2hm5opbye2ghmz2ljy fingerprint_ignore_patterns: [] diff --git a/packages/valory/skills/optimus_abci/skill.yaml b/packages/valory/skills/optimus_abci/skill.yaml index a02a9e4..35a9a35 100644 --- a/packages/valory/skills/optimus_abci/skill.yaml +++ b/packages/valory/skills/optimus_abci/skill.yaml @@ -7,12 +7,12 @@ license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeiechr3zr5bc4xl3vvs2p2gti54ily7ao2gu4ff5lys6cixehzkdea - behaviours.py: bafybeidutyxryf6h7ooc7ggrdl5lojfv3anrvf3zq4tbwkiwxm7qqrriye - composition.py: bafybeiczokwfi2h6qkw4lk5cp52tafddxxysh6qwrnzqt4cfu67yb5a2nq - dialogues.py: bafybeift7d4mk4qkzm4rqmdrfv43erhunzyqmwn6pecuumq25amy7q7fp4 + behaviours.py: bafybeihqhdnqza5f27soi4pf3x3bxqcachbsmx7zceqcw6uv2obgynxrm4 + composition.py: bafybeibiyxe22c5c33keuosha2t6b4zez2iqghawbtfhumqrubrtsuun4y + dialogues.py: bafybeihhgzlutexxt6ry74m2rlaxdhu3ldbvhugmmgxxjlfcxf4xaipisy fsm_specification.yaml: bafybeiasafyyxit3jv5tpd73knlw245oge357yj3af6lzvowmu5gqemic4 - handlers.py: bafybeibjxph27kmpljyeqkednkrfgrtelksfs3kbgjbvh6eycjrfqz5o2y - models.py: bafybeibbnyxant6quqcbzwebu5jyws46mjpvpvsxnr3yrnq6slhrbd6zsa + handlers.py: bafybeidmh47f6uk3rvgh6ivvb2v43mxugiujoee6zxofhclqoyt7u7r524 + models.py: bafybeidczdw3drlmdaetc3rvlvniyc6etrdvpdzhlgmvgxj6stfue7vqoy fingerprint_ignore_patterns: [] connections: [] contracts: [] @@ -25,7 +25,7 @@ skills: - valory/registration_abci:0.1.0:bafybeiffipsowrqrkhjoexem7ern5ob4fabgif7wa6gtlszcoaop2e3oey - valory/reset_pause_abci:0.1.0:bafybeif4lgvbzsmzljesxbphycdv52ka7qnihyjrjpfaseclxadcmm6yiq - valory/termination_abci:0.1.0:bafybeiekkpo5qef5zaeagm3si6v45qxcojvtjqe4a5ceccvk4q7k3xi3bi -- valory/liquidity_trader_abci:0.1.0:bafybeibxgayrhcuc6r63fjvo2zvuurby4d3weasoccyjad5c4el5logcfq +- valory/liquidity_trader_abci:0.1.0:bafybeid7yambarhmg5xnlwxvhqwmwjmvf3wr7a3zzzicmac7buroq5d4we - valory/transaction_settlement_abci:0.1.0:bafybeielv6eivt2z6nforq43xewl2vmpfwpdu2s2vfogobziljnwsclmlm - valory/market_data_fetcher_abci:0.1.0:bafybeies3ib3ft3tqepogstxjarq3ku4xepnfozyvg2rur5ztq4afchgcq - valory/trader_decision_maker_abci:0.1.0:bafybeicskzcqr7dd4d5ulwss5u3hsx2wvcxatugtxiuja2neobbr3tqtkm From 53a17faf17f544d8dafb27ea07f5e48e170b5b38 Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Tue, 29 Oct 2024 19:33:38 +0530 Subject: [PATCH 34/41] chore: Ignore mypy errors for eightballer package --- tox.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tox.ini b/tox.ini index ffbb64c..f8b923f 100644 --- a/tox.ini +++ b/tox.ini @@ -403,6 +403,9 @@ ignore_errors=True [mypy-packages.valory.protocols.*] ignore_errors=True +[mypy-packages.eightballer.*] +ignore_errors=True + [mypy-packages.valory.skills.abstract_abci.*] ignore_errors=True From d21eed6159535ee4908c6927873fac8f4216d578 Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Tue, 29 Oct 2024 22:55:48 +0530 Subject: [PATCH 35/41] chore: Update service.yaml file with new parameters and values --- packages/valory/services/optimus/service.yaml | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/packages/valory/services/optimus/service.yaml b/packages/valory/services/optimus/service.yaml index 20843b9..737c691 100644 --- a/packages/valory/services/optimus/service.yaml +++ b/packages/valory/services/optimus/service.yaml @@ -10,12 +10,24 @@ agent: valory/optimus:0.1.0:bafybeie2eu7yzb6i4spwgq63zo6jdpbsxam5nlw4ridiatw5jwj number_of_agents: 1 deployment: {} --- +public_id: valory/ipfs_package_downloader:0.1.0 +type: skill +models: + params: + args: + cleanup_freq: ${CLEANUP_FREQ:int:50} + timeout_limit: ${TIMEOUT_LIMIT:int:3} + file_hash_to_id: ${FILE_HASH_TO_ID:list:[["bafybeifpqcxwjjlenpa7n3nnx5ornrg7uz5d7h76ugzhiwk45bx3sx3cta",["sma_strategy"]]]} + component_yaml_filename: ${COMPONENT_YAML_FILENAME:str:component.yaml} + entry_point_key: ${ENTRY_POINT_KEY:str:entry_point} + callable_keys: ${CALLABLE_KEYS:list:["run_callable","transform_callable","evaluate_callable"]} +--- public_id: valory/optimus_abci:0.1.0 type: skill models: benchmark_tool: args: - log_dir: ${LOG_DIR:str:/logs} + log_dir: ${LOG_DIR:str:/Users/gauravlochab/Documents/Code/optimus/logs} params: args: setup: @@ -67,7 +79,7 @@ models: tenderly_access_key: ${TENDERLY_ACCESS_KEY:str:access_key} tenderly_account_slug: ${TENDERLY_ACCOUNT_SLUG:str:account_slug} tenderly_project_slug: ${TENDERLY_PROJECT_SLUG:str:project_slug} - agent_transition: ${AGENT_TRANSITION:bool:agent_transition} + agent_transition: ${AGENT_TRANSITION:bool:true} tendermint_p2p_url: ${TENDERMINT_P2P_URL_0:str:optimism_tm_0:26656} service_endpoint_base: ${SERVICE_ENDPOINT_BASE:str:https://optimism.autonolas.tech/} multisend_batch_size: ${MULTISEND_BATCH_SIZE:int:5} @@ -85,6 +97,25 @@ models: max_gas_percentage: ${MAX_GAS_PERCENTAGE:float:0.25} balancer_graphql_endpoints: ${BALANCER_GRAPHQL_ENDPOINTS:str:{"optimism":"https://api.studio.thegraph.com/query/75376/balancer-optimism-v2/version/latest","base":"https://api.studio.thegraph.com/query/24660/balancer-base-v2/version/latest"}} allowed_chains: ${ALLOWED_CHAINS:list:["optimism","base"]} + tx_timeout: ${TX_TIMEOUT:float:10.0} + use_termination: ${USE_TERMINATION:bool:false} + validate_timeout: ${VALIDATE_TIMEOUT:int:1205} + history_check_timeout: ${HISTORY_CHECK_TIMEOUT:int:1205} + token_symbol_whitelist: ${TOKEN_SYMBOL_WHITELIST:list:["coingecko_id=solana&address=So11111111111111111111111111111111111111112"]} + strategies_kwargs: ${STRATEGIES_KWARGS:list:[["ma_period",20],["rsi_period",14],["rsi_overbought_threshold",70],["rsi_oversold_threshold",30]]} + use_proxy_server: ${USE_PROXY_SERVER:bool:false} + expected_swap_tx_cost: ${EXPECTED_SWAP_TX_COST:int:20000000} + ipfs_fetch_retries: ${IPFS_FETCH_RETRIES:int:5} + squad_vault: ${SQUAD_VAULT:str:39Zh4C687EXLY7CT8gjCxe2hUc3krESjUsqs7A1CKD5E} + agent_balance_threshold: ${AGENT_BALANCE_THRESHOLD:int:50000000} + multisig_balance_threshold: ${MULTISIG_BALANCE_THRESHOLD:int:1000000000} + tracked_tokens: ${TRACKED_TOKENS:list:[]} + refill_action_timeout: ${REFILL_ACTION_TIMEOUT:int:10} + rpc_polling_interval: ${RPC_POLLING_INTERVAL:int:5} + epsilon: ${EPSILON:float:0.1} + sharpe_threshold: ${SHARPE_THRESHOLD:float:1.0} + ledger_ids: ${LEDGER_IDS:list:["ethereum"]} + trade_size_in_base_token: ${TRADE_SIZE_IN_BASE_TOKEN:float:0.0001} coingecko: args: token_price_endpoint: ${COINGECKO_TOKEN_PRICE_ENDPOINT:str:https://api.coingecko.com/api/v3/simple/token_price/{asset_platform_id}?contract_addresses={token_address}&vs_currencies=usd} @@ -94,6 +125,20 @@ models: credits: ${COINGECKO_CREDITS:int:10000} rate_limited_code: ${COINGECKO_RATE_LIMITED_CODE:int:429} chain_to_platform_id_mapping: ${COINGECKO_CHAIN_TO_PLATFORM_ID_MAPPING:str:{"optimism":"optimistic-ethereum","base":"base","ethereum":"ethereum"}} + prices_field: ${COINGECKO_PRICES_FIELD:str:prices} + endpoint: ${COINGECKO_ENDPOINT:str:https://api.coingecko.com/api/v3/coins/{token_id}/market_chart?vs_currency=usd&days=1} + tx_settlement_proxy: + args: + parameters: + amount: ${TX_PROXY_SWAP_AMOUNT:int:100000000} + slippageBps: ${TX_PROXY_SLIPPAGE_BPS:int:5} + resendAmount: ${TX_PROXY_SPAM_AMOUNT:int:200} + timeoutInMs: ${TX_PROXY_VERIFICATION_TIMEOUT_MS:int:120000} + priorityFee: ${TX_PROXY_PRIORITY_FEE:int:5000000} + response_key: ${TX_PROXY_RESPONSE_KEY:str:null} + response_type: ${TX_PROXY_RESPONSE_TYPE:str:dict} + retries: ${TX_PROXY_RETRIES:int:5} + url: ${TX_PROXY_URL:str:http://localhost:3000/tx} --- public_id: valory/ledger:0.19.0 type: connection From f4b2a80eeae594d196ff8113d75e97ff7a61a2b9 Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Tue, 29 Oct 2024 23:17:04 +0530 Subject: [PATCH 36/41] chore: Update agent and service configurations --- packages/packages.json | 4 ++-- packages/valory/agents/optimus/aea-config.yaml | 8 ++++---- packages/valory/services/optimus/service.yaml | 2 +- pyproject.toml | 1 + 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/packages.json b/packages/packages.json index a38ffb5..0219345 100644 --- a/packages/packages.json +++ b/packages/packages.json @@ -10,8 +10,8 @@ "contract/valory/staking_activity_checker/0.1.0": "bafybeibjzsi2r5b6xd4iwl4wbwldptnynryzsdpifym4mkv32ynswx22ou", "skill/valory/liquidity_trader_abci/0.1.0": "bafybeid7yambarhmg5xnlwxvhqwmwjmvf3wr7a3zzzicmac7buroq5d4we", "skill/valory/optimus_abci/0.1.0": "bafybeifjkfbj7symbsm73e3dxf6yv3nfxmaqkpi3szqexsv2zepeiea4fm", - "agent/valory/optimus/0.1.0": "bafybeie2eu7yzb6i4spwgq63zo6jdpbsxam5nlw4ridiatw5jwjjm4qxu4", - "service/valory/optimus/0.1.0": "bafybeib6fnkhtsayqps6vmknjlne6lnx7yb24wkrkijv6chcdm4lzalfc4" + "agent/valory/optimus/0.1.0": "bafybeibhxa4mapznr5epbnr4q3kuhofc72spmp4n54ufl3avf5rjkfdq4m", + "service/valory/optimus/0.1.0": "bafybeihujq5vi3ovkjr46h5stbqpc3a324mv7d676zh75xw3ifu2eq2wm4" }, "third_party": { "custom/eightballer/rsi_strategy/0.1.0": "bafybeigbofp2nqwcxu3rlkuugpc3w6ils3u7glse7c335rddcqg56ybh34", diff --git a/packages/valory/agents/optimus/aea-config.yaml b/packages/valory/agents/optimus/aea-config.yaml index 61d0202..9aafe69 100644 --- a/packages/valory/agents/optimus/aea-config.yaml +++ b/packages/valory/agents/optimus/aea-config.yaml @@ -152,7 +152,7 @@ type: skill models: benchmark_tool: args: - log_dir: ${str:/logs} + log_dir: ${str:/Users/gauravlochab/Documents/Code/optimus/logs} get_balance: args: api_id: ${str:get_balance} @@ -246,7 +246,7 @@ models: service_id: optimus service_registry_address: ${str:null} setup: - all_participants: ${list:["0x08012c56eD8adF43586A3cEf68EEb13FDfF70Ef5"]} + all_participants: ${list:["0x1aCD50F973177f4D320913a9Cc494A9c66922fdF"]} consensus_threshold: ${int:null} safe_contract_address: ${str:0xc4ed6F4B3059AD6aa985A9e47C133a45A39db66e} share_tm_config_on_startup: ${bool:false} @@ -298,12 +298,12 @@ models: tenderly_access_key: ${str:access_key} tenderly_account_slug: ${str:account_slug} tenderly_project_slug: ${str:project_slug} - agent_transition: ${bool:True} + agent_transition: ${bool:False} chain_to_chain_id_mapping: ${str:{"optimism":10,"base":8453,"ethereum":1}} staking_token_contract_address: ${str:0x88996bbdE7f982D93214881756840cE2c77C4992} staking_activity_checker_contract_address: ${str:0x7Fd1F4b764fA41d19fe3f63C85d12bf64d2bbf68} staking_threshold_period: ${int:5} - store_path: ${str:/data/} + store_path: ${str:/Users/gauravlochab/Documents/Code/optimus/data/} assets_info_filename: ${str:assets.json} pool_info_filename: ${str:current_pool.json} gas_cost_info_filename: ${str:gas_costs.json} diff --git a/packages/valory/services/optimus/service.yaml b/packages/valory/services/optimus/service.yaml index 737c691..254d1c0 100644 --- a/packages/valory/services/optimus/service.yaml +++ b/packages/valory/services/optimus/service.yaml @@ -6,7 +6,7 @@ aea_version: '>=1.0.0, <2.0.0' license: Apache-2.0 fingerprint: {} fingerprint_ignore_patterns: [] -agent: valory/optimus:0.1.0:bafybeie2eu7yzb6i4spwgq63zo6jdpbsxam5nlw4ridiatw5jwjjm4qxu4 +agent: valory/optimus:0.1.0:bafybeibhxa4mapznr5epbnr4q3kuhofc72spmp4n54ufl3avf5rjkfdq4m number_of_agents: 1 deployment: {} --- diff --git a/pyproject.toml b/pyproject.toml index 95f550e..090339f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,6 +64,7 @@ open-balpy = "==0.0.7" numpy = "==1.26.1" dateparser = ">=1.1.1" open-multicaller = "==0.2.0" +pydantic = "^2.9.2" [tool.poetry.group.dev.dependencies] pytest-xprocess = ">=0.18.1,<0.19.0" From 9da59d8ea19c4974dfd7eacab2a65e50ff63456d Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Wed, 30 Oct 2024 02:52:36 +0530 Subject: [PATCH 37/41] chore: Update aea-config-replace.py to include new environment variables --- scripts/aea-config-replace.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/scripts/aea-config-replace.py b/scripts/aea-config-replace.py index 7a903dc..aa419fe 100644 --- a/scripts/aea-config-replace.py +++ b/scripts/aea-config-replace.py @@ -53,44 +53,45 @@ def main() -> None: # Params try: - config[5]["models"]["params"]["args"]["setup"][ + config[6]["models"]["params"]["args"]["setup"][ "all_participants" ] = f"${{list:{os.getenv('ALL_PARTICIPANTS')}}}" - config[5]["models"]["params"]["args"][ + config[6]["models"]["params"]["args"][ "safe_contract_addresses" ] = f"${{str:{os.getenv('SAFE_CONTRACT_ADDRESSES')}}}" - config[5]["models"]["params"]["args"][ + config[6]["models"]["params"]["args"][ "slippage_for_swap" ] = f"${{float:{os.getenv('SLIPPAGE_FOR_SWAP')}}}" - config[5]["models"]["params"]["args"][ + config[6]["models"]["params"]["args"][ "tenderly_access_key" ] = f"${{str:{os.getenv('TENDERLY_ACCESS_KEY')}}}" - config[5]["models"]["params"]["args"][ + config[6]["models"]["params"]["args"][ "agent_transition" ] = f"${{bool:{os.getenv('AGENT_TRANSITION')}}}" - config[5]["models"]["params"]["args"][ + config[6]["models"]["params"]["args"][ "tenderly_account_slug" ] = f"${{str:{os.getenv('TENDERLY_ACCOUNT_SLUG')}}}" - config[5]["models"]["params"]["args"][ + config[6]["models"]["params"]["args"][ "tenderly_project_slug" ] = f"${{str:{os.getenv('TENDERLY_PROJECT_SLUG')}}}" - config[5]["models"]["coingecko"]["args"][ + config[6]["models"]["coingecko"]["args"][ "api_key" ] = f"${{str:{os.getenv('COINGECKO_API_KEY')}}}" + + config[6]["models"]["params"]["args"][ + "allowed_chains" + ] = f"${{list:{os.getenv('ALLOWED_CHAINS')}}}" + except KeyError as e: print("Error", e) - config[5]["models"]["params"]["args"][ - "allowed_chains" - ] = f"${{list:{os.getenv('ALLOWED_CHAINS')}}}" - with open(Path("optimus", "aea-config.yaml"), "w", encoding="utf-8") as file: yaml.dump_all(config, file, sort_keys=False) From 56d8dab311062ae01223ba08d1efee02dd9400d1 Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Wed, 30 Oct 2024 03:07:05 +0530 Subject: [PATCH 38/41] chore: Update service.yaml file with new parameters and values --- packages/packages.json | 2 +- packages/valory/services/optimus/service.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/packages.json b/packages/packages.json index 0219345..9203741 100644 --- a/packages/packages.json +++ b/packages/packages.json @@ -11,7 +11,7 @@ "skill/valory/liquidity_trader_abci/0.1.0": "bafybeid7yambarhmg5xnlwxvhqwmwjmvf3wr7a3zzzicmac7buroq5d4we", "skill/valory/optimus_abci/0.1.0": "bafybeifjkfbj7symbsm73e3dxf6yv3nfxmaqkpi3szqexsv2zepeiea4fm", "agent/valory/optimus/0.1.0": "bafybeibhxa4mapznr5epbnr4q3kuhofc72spmp4n54ufl3avf5rjkfdq4m", - "service/valory/optimus/0.1.0": "bafybeihujq5vi3ovkjr46h5stbqpc3a324mv7d676zh75xw3ifu2eq2wm4" + "service/valory/optimus/0.1.0": "bafybeihnqua7fx7oluwbpw56rye3qf42gn2vhg46bdxxb36qpsoexenpvm" }, "third_party": { "custom/eightballer/rsi_strategy/0.1.0": "bafybeigbofp2nqwcxu3rlkuugpc3w6ils3u7glse7c335rddcqg56ybh34", diff --git a/packages/valory/services/optimus/service.yaml b/packages/valory/services/optimus/service.yaml index 254d1c0..e4f88d7 100644 --- a/packages/valory/services/optimus/service.yaml +++ b/packages/valory/services/optimus/service.yaml @@ -27,7 +27,7 @@ type: skill models: benchmark_tool: args: - log_dir: ${LOG_DIR:str:/Users/gauravlochab/Documents/Code/optimus/logs} + log_dir: ${LOG_DIR:str:/logs} params: args: setup: @@ -79,7 +79,7 @@ models: tenderly_access_key: ${TENDERLY_ACCESS_KEY:str:access_key} tenderly_account_slug: ${TENDERLY_ACCOUNT_SLUG:str:account_slug} tenderly_project_slug: ${TENDERLY_PROJECT_SLUG:str:project_slug} - agent_transition: ${AGENT_TRANSITION:bool:true} + agent_transition: ${AGENT_TRANSITION:bool:False} tendermint_p2p_url: ${TENDERMINT_P2P_URL_0:str:optimism_tm_0:26656} service_endpoint_base: ${SERVICE_ENDPOINT_BASE:str:https://optimism.autonolas.tech/} multisend_batch_size: ${MULTISEND_BATCH_SIZE:int:5} From 3e0c83cea6206ed9b1a7ab82bff3be163fbbdcab Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Wed, 30 Oct 2024 03:09:17 +0530 Subject: [PATCH 39/41] chore: resolving ci checks --- scripts/aea-config-replace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/aea-config-replace.py b/scripts/aea-config-replace.py index aa419fe..14b7018 100644 --- a/scripts/aea-config-replace.py +++ b/scripts/aea-config-replace.py @@ -88,7 +88,7 @@ def main() -> None: config[6]["models"]["params"]["args"][ "allowed_chains" ] = f"${{list:{os.getenv('ALLOWED_CHAINS')}}}" - + except KeyError as e: print("Error", e) From fce018dfb947c4edb984fb34270008eee0b73de3 Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Wed, 30 Oct 2024 11:37:08 +0530 Subject: [PATCH 40/41] chore: add default flag and readme for merge --- Merge_Readme.md | 176 ++++++++++++++++++++++++++++++++++ scripts/aea-config-replace.py | 2 +- 2 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 Merge_Readme.md diff --git a/Merge_Readme.md b/Merge_Readme.md new file mode 100644 index 0000000..4673423 --- /dev/null +++ b/Merge_Readme.md @@ -0,0 +1,176 @@ +# Run Your Own Agent + +This guide will help you set up and run your own agent, either **Optimus** or **BabyDegen**. Follow the steps below to get started. + +--- + +## Table of Contents + +1. [Get the Code](#1-get-the-code) +2. [Set Up the Virtual Environment](#2-set-up-the-virtual-environment) +3. [Synchronize Packages](#3-synchronize-packages) +4. [Prepare the Data](#4-prepare-the-data) +5. [Configure for Optimus](#5-configure-for-optimus) +6. [Configure for BabyDegen](#6-configure-for-babydegen) +7. [Run the Agent](#7-run-the-agent) + +--- + +## 1. Get the Code + +Clone the repository from GitHub: + +```bash +git clone https://github.com/valory-xyz/optimus.git +``` + +--- + +## 2. Set Up the Virtual Environment + +Navigate to the project directory and install the required dependencies using `poetry`: + +```bash +cd optimus +poetry install +poetry shell +``` + +--- + +## 3. Synchronize Packages + +Synchronize the necessary packages: + +```bash +autonomy packages sync --update-packages +``` + +--- + +## 4. Prepare the Data + +### Generate Wallet Keys + +Create a `keys.json` file containing wallet addresses and private keys for four agents: + +```bash +autonomy generate-key ethereum -n 4 +``` + +### Create Ethereum Private Key File + +Extract one of the private keys from `keys.json` and save it in a file named `ethereum_private_key.txt`. Ensure there's **no newline at the end of the file**. + +--- + +## 5. Configure for Optimus + +If you want to run the **Optimus** agent, follow these steps: + +### a. Deploy Safe Contracts + +Deploy [Safe](https://safe.global/) contracts on the following networks: + +- Ethereum Mainnet +- Optimism +- Base +- Mode + +### b. Fund Your Safe and Agent Addresses + +- **Safe Addresses**: + - Deposit **ETH** and **USDC** into your Safe address on **Ethereum Mainnet**. +- **Agent Addresses**: + - Deposit **ETH** into your agent addresses on all networks (Ethereum Mainnet, Optimism, Base, Mode) to cover gas fees. + +### c. Obtain API Keys + +- **Tenderly**: + - Access Key + - Account Slug + - Project Slug + - Get them from your [Tenderly Dashboard](https://dashboard.tenderly.co/) under **Settings**. +- **CoinGecko**: + - API Key + - Obtain it from your account's [Developer Dashboard](https://www.coingecko.com/account/dashboard). + +### d. Set Environment Variables + +Replace placeholder values with your actual data: + +```bash +export ETHEREUM_LEDGER_RPC=YOUR_ETHEREUM_RPC_URL +export OPTIMISM_LEDGER_RPC=YOUR_OPTIMISM_RPC_URL +export BASE_LEDGER_RPC=YOUR_BASE_RPC_URL + +export ALL_PARTICIPANTS='["YOUR_AGENT_ADDRESS"]' +export SAFE_CONTRACT_ADDRESSES='{ + "ethereum": "YOUR_SAFE_ADDRESS_ON_ETHEREUM", + "optimism": "YOUR_SAFE_ADDRESS_ON_OPTIMISM", + "base": "YOUR_SAFE_ADDRESS_ON_BASE", + "mode": "YOUR_SAFE_ADDRESS_ON_MODE" +}' + +export SLIPPAGE_FOR_SWAP=0.09 +export TENDERLY_ACCESS_KEY=YOUR_TENDERLY_ACCESS_KEY +export TENDERLY_ACCOUNT_SLUG=YOUR_TENDERLY_ACCOUNT_SLUG +export TENDERLY_PROJECT_SLUG=YOUR_TENDERLY_PROJECT_SLUG +export COINGECKO_API_KEY=YOUR_COINGECKO_API_KEY +``` + +--- + +## 6. Configure for BabyDegen + +If you prefer to run the **BabyDegen** agent, follow these additional steps: + +### a. Set the AGENT_TRANSITION Flag + +Set the `AGENT_TRANSITION` flag to `true` in your environment or configuration files. + +### b. Update Safe Address and Participants + +Replace placeholders with your actual Safe address and agent address: + +```bash +export SAFE_ADDRESS="YOUR_SAFE_ADDRESS" +export ALL_PARTICIPANTS='["YOUR_AGENT_ADDRESS"]' +``` + +### c. Fund the Safe Account + +Deposit the following tokens into your Safe account, depending on the network: + +```python +LEDGER_TO_TOKEN_LIST = { + SupportedLedgers.ETHEREUM: [ + "0x0001a500a6b18995b03f44bb040a5ffc28e45cb0", # Token A on Ethereum + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", # USDC on Ethereum + "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", # WETH on Ethereum + ], + SupportedLedgers.OPTIMISM: [ + "0x4200000000000000000000000000000000000006", # WETH on Optimism + "0x0b2c639c533813f4aa9d7837caf62653d097ff85", # Token B on Optimism + "0xda10009cbd5d07dd0cecc66161fc93d7c9000da1", # DAI on Optimism + ], + SupportedLedgers.BASE: [ + "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca", # Token C on Base + "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", # Token D on Base + ], +} +``` + +### d. Fund the Agent Address + +Ensure your agent address has enough native tokens to cover gas fees on your chosen network. + +--- + +## 7. Run the Agent + +After completing the setup for either **Optimus** or **BabyDegen**, run the agent using the provided script: + +```bash +bash run_agent.sh +``` diff --git a/scripts/aea-config-replace.py b/scripts/aea-config-replace.py index 14b7018..fd6cc3a 100644 --- a/scripts/aea-config-replace.py +++ b/scripts/aea-config-replace.py @@ -71,7 +71,7 @@ def main() -> None: config[6]["models"]["params"]["args"][ "agent_transition" - ] = f"${{bool:{os.getenv('AGENT_TRANSITION')}}}" + ] = f"${{bool:{os.getenv('AGENT_TRANSITION', 'False') == 'True'}}}" config[6]["models"]["params"]["args"][ "tenderly_account_slug" From f3fe392da743c0dc1df1c3f801296ca8fd50f852 Mon Sep 17 00:00:00 2001 From: gauravlochab Date: Wed, 30 Oct 2024 11:46:35 +0530 Subject: [PATCH 41/41] chore: Update merge_readme --- Merge_Readme.md | 53 +++++++++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/Merge_Readme.md b/Merge_Readme.md index 4673423..8390d6e 100644 --- a/Merge_Readme.md +++ b/Merge_Readme.md @@ -75,14 +75,13 @@ Deploy [Safe](https://safe.global/) contracts on the following networks: - Ethereum Mainnet - Optimism - Base -- Mode ### b. Fund Your Safe and Agent Addresses - **Safe Addresses**: - Deposit **ETH** and **USDC** into your Safe address on **Ethereum Mainnet**. - **Agent Addresses**: - - Deposit **ETH** into your agent addresses on all networks (Ethereum Mainnet, Optimism, Base, Mode) to cover gas fees. + - Deposit **ETH** into your agent addresses on all networks (Ethereum Mainnet, Optimism, Base) to cover gas fees. ### c. Obtain API Keys @@ -95,30 +94,32 @@ Deploy [Safe](https://safe.global/) contracts on the following networks: - API Key - Obtain it from your account's [Developer Dashboard](https://www.coingecko.com/account/dashboard). -### d. Set Environment Variables +### d. Create a `.env` File -Replace placeholder values with your actual data: +Instead of exporting environment variables, create a `.env` file in your project directory and add the following configurations. Replace placeholder values with your actual data: -```bash -export ETHEREUM_LEDGER_RPC=YOUR_ETHEREUM_RPC_URL -export OPTIMISM_LEDGER_RPC=YOUR_OPTIMISM_RPC_URL -export BASE_LEDGER_RPC=YOUR_BASE_RPC_URL - -export ALL_PARTICIPANTS='["YOUR_AGENT_ADDRESS"]' -export SAFE_CONTRACT_ADDRESSES='{ +```dotenv +PYTHONWARNINGS="ignore" +ALL_PARTICIPANTS=["YOUR_AGENT_ADDRESS"] +SAFE_CONTRACT_ADDRESSES='{ "ethereum": "YOUR_SAFE_ADDRESS_ON_ETHEREUM", "optimism": "YOUR_SAFE_ADDRESS_ON_OPTIMISM", - "base": "YOUR_SAFE_ADDRESS_ON_BASE", - "mode": "YOUR_SAFE_ADDRESS_ON_MODE" + "base": "YOUR_SAFE_ADDRESS_ON_BASE" }' - -export SLIPPAGE_FOR_SWAP=0.09 -export TENDERLY_ACCESS_KEY=YOUR_TENDERLY_ACCESS_KEY -export TENDERLY_ACCOUNT_SLUG=YOUR_TENDERLY_ACCOUNT_SLUG -export TENDERLY_PROJECT_SLUG=YOUR_TENDERLY_PROJECT_SLUG -export COINGECKO_API_KEY=YOUR_COINGECKO_API_KEY +ETHEREUM_LEDGER_RPC=YOUR_ETHEREUM_RPC_URL +OPTIMISM_LEDGER_RPC=YOUR_OPTIMISM_RPC_URL +BASE_LEDGER_RPC=YOUR_BASE_RPC_URL +MODE_LEDGER_RPC= +SLIPPAGE_FOR_SWAP=0.09 +TENDERLY_ACCESS_KEY=YOUR_TENDERLY_ACCESS_KEY +TENDERLY_ACCOUNT_SLUG=YOUR_TENDERLY_ACCOUNT_SLUG +TENDERLY_PROJECT_SLUG=YOUR_TENDERLY_PROJECT_SLUG +COINGECKO_API_KEY=YOUR_COINGECKO_API_KEY +ALLOWED_CHAINS=["optimism","base"] ``` +**Note:** Ensure you remove any references to the **Mode** network, as it's not required for Optimus. + --- ## 6. Configure for BabyDegen @@ -127,15 +128,19 @@ If you prefer to run the **BabyDegen** agent, follow these additional steps: ### a. Set the AGENT_TRANSITION Flag -Set the `AGENT_TRANSITION` flag to `true` in your environment or configuration files. +Set the `AGENT_TRANSITION` flag to `true` in your `.env` file: + +```dotenv +AGENT_TRANSITION=true +``` ### b. Update Safe Address and Participants Replace placeholders with your actual Safe address and agent address: -```bash -export SAFE_ADDRESS="YOUR_SAFE_ADDRESS" -export ALL_PARTICIPANTS='["YOUR_AGENT_ADDRESS"]' +```dotenv +SAFE_ADDRESS="YOUR_SAFE_ADDRESS" +ALL_PARTICIPANTS=["YOUR_AGENT_ADDRESS"] ``` ### c. Fund the Safe Account @@ -173,4 +178,4 @@ After completing the setup for either **Optimus** or **BabyDegen**, run the agen ```bash bash run_agent.sh -``` +``` \ No newline at end of file