From b9861d566f37a30d735e224dbd8b4d31f170fb50 Mon Sep 17 00:00:00 2001 From: abel Date: Fri, 18 Aug 2023 00:55:26 -0300 Subject: [PATCH 01/17] (feat) Added a component to calculate gas limit for the Transaction Broadcaster based on the messages (without running the transaction simulation) --- .../46_MessageBroadcasterWithoutSimulation.py | 64 +++++ ...sterWithGranteeAccountWithoutSimulation.py | 54 ++++ .../explorer_rpc/1_GetTxByHash.py | 4 +- pyinjective/core/broadcaster.py | 155 +++++++++++- ...essage_based_transaction_fee_calculator.py | 231 ++++++++++++++++++ 5 files changed, 503 insertions(+), 5 deletions(-) create mode 100644 examples/chain_client/46_MessageBroadcasterWithoutSimulation.py create mode 100644 examples/chain_client/47_MessageBroadcasterWithGranteeAccountWithoutSimulation.py create mode 100644 tests/core/test_message_based_transaction_fee_calculator.py diff --git a/examples/chain_client/46_MessageBroadcasterWithoutSimulation.py b/examples/chain_client/46_MessageBroadcasterWithoutSimulation.py new file mode 100644 index 00000000..847c79a9 --- /dev/null +++ b/examples/chain_client/46_MessageBroadcasterWithoutSimulation.py @@ -0,0 +1,64 @@ +import asyncio + +from pyinjective.composer import Composer as ProtoMsgComposer +from pyinjective.core.broadcaster import MsgBroadcasterWithPk +from pyinjective.constant import Network +from pyinjective.wallet import PrivateKey + + +async def main() -> None: + # select network: local, testnet, mainnet + network = Network.testnet() + composer = ProtoMsgComposer(network=network.string()) + private_key_in_hexa = "f9db9bf330e23cb7839039e944adef6e9df447b90b503d5b4464c90bea9022f3" + + message_broadcaster = MsgBroadcasterWithPk.new_without_simulation( + network=network, + private_key=private_key_in_hexa, + use_secure_connection=True + ) + + priv_key = PrivateKey.from_hex(private_key_in_hexa) + pub_key = priv_key.to_public_key() + address = pub_key.to_address() + subaccount_id = address.get_subaccount_id(index=0) + + # prepare trade info + fee_recipient = "inj1hkhdaj2a2clmq5jq6mspsggqs32vynpk228q3r" + + spot_market_id_create = "0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe" + + spot_orders_to_create = [ + composer.SpotOrder( + market_id=spot_market_id_create, + subaccount_id=subaccount_id, + fee_recipient=fee_recipient, + price=3, + quantity=55, + is_buy=True, + is_po=False + ), + composer.SpotOrder( + market_id=spot_market_id_create, + subaccount_id=subaccount_id, + fee_recipient=fee_recipient, + price=300, + quantity=55, + is_buy=False, + is_po=False + ), + ] + + # prepare tx msg + msg = composer.MsgBatchUpdateOrders( + sender=address.to_acc_bech32(), + spot_orders_to_create=spot_orders_to_create, + ) + + # broadcast the transaction + result = await message_broadcaster.broadcast([msg]) + print("---Transaction Response---") + print(result) + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/47_MessageBroadcasterWithGranteeAccountWithoutSimulation.py b/examples/chain_client/47_MessageBroadcasterWithGranteeAccountWithoutSimulation.py new file mode 100644 index 00000000..b3311279 --- /dev/null +++ b/examples/chain_client/47_MessageBroadcasterWithGranteeAccountWithoutSimulation.py @@ -0,0 +1,54 @@ +import asyncio + +from pyinjective.composer import Composer as ProtoMsgComposer +from pyinjective.async_client import AsyncClient +from pyinjective.core.broadcaster import MsgBroadcasterWithPk +from pyinjective.constant import Network +from pyinjective.wallet import PrivateKey, Address + + +async def main() -> None: + # select network: local, testnet, mainnet + network = Network.testnet() + composer = ProtoMsgComposer(network=network.string()) + + # initialize grpc client + client = AsyncClient(network, insecure=False) + await client.sync_timeout_height() + + # load account + private_key_in_hexa = "5d386fbdbf11f1141010f81a46b40f94887367562bd33b452bbaa6ce1cd1381e" + priv_key = PrivateKey.from_hex(private_key_in_hexa) + pub_key = priv_key.to_public_key() + address = pub_key.to_address() + + message_broadcaster = MsgBroadcasterWithPk.new_for_grantee_account_without_simulation( + network=network, + grantee_private_key=private_key_in_hexa, + use_secure_connection=True + ) + + # prepare tx msg + market_id = "0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe" + granter_inj_address = "inj1hkhdaj2a2clmq5jq6mspsggqs32vynpk228q3r" + granter_address = Address.from_acc_bech32(granter_inj_address) + granter_subaccount_id = granter_address.get_subaccount_id(index=0) + + msg = composer.MsgCreateSpotLimitOrder( + sender=granter_inj_address, + market_id=market_id, + subaccount_id=granter_subaccount_id, + fee_recipient=address.to_acc_bech32(), + price=7.523, + quantity=0.01, + is_buy=True, + is_po=False + ) + + # broadcast the transaction + result = await message_broadcaster.broadcast([msg]) + print("---Transaction Response---") + print(result) + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/exchange_client/explorer_rpc/1_GetTxByHash.py b/examples/exchange_client/explorer_rpc/1_GetTxByHash.py index 7362d1e0..fa21e293 100644 --- a/examples/exchange_client/explorer_rpc/1_GetTxByHash.py +++ b/examples/exchange_client/explorer_rpc/1_GetTxByHash.py @@ -7,10 +7,10 @@ async def main() -> None: # select network: local, testnet, mainnet - network = Network.testnet() + network = Network.mainnet() client = AsyncClient(network, insecure=False) composer = Composer(network=network.string()) - tx_hash = "0F3EBEC1882E1EEAC5B7BDD836E976250F1CD072B79485877CEACCB92ACDDF52" + tx_hash = "50DD80270052D85835939A393127B5626917007E71A7ABD5205F1A094B976C1B" transaction_response = await client.get_tx_by_hash(tx_hash=tx_hash) print(transaction_response) diff --git a/pyinjective/core/broadcaster.py b/pyinjective/core/broadcaster.py index d213d8de..fb9f7eed 100644 --- a/pyinjective/core/broadcaster.py +++ b/pyinjective/core/broadcaster.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod from decimal import Decimal -from typing import List, Optional +from typing import Callable, List, Optional, Tuple import math from google.protobuf import any_pb2 @@ -9,6 +9,7 @@ from pyinjective.async_client import AsyncClient from pyinjective.composer import Composer from pyinjective.constant import Network +from pyinjective.proto.cosmos.authz.v1beta1 import tx_pb2 as cosmos_authz_tx_pb class BroadcasterAccountConfig(ABC): @@ -86,6 +87,31 @@ def new_using_simulation( ) return instance + @classmethod + def new_without_simulation( + cls, + network: Network, + private_key: str, + use_secure_connection: bool = True, + client: Optional[AsyncClient] = None, + composer: Optional[Composer] = None, + ): + client = client or AsyncClient(network=network, insecure=not (use_secure_connection)) + composer = composer or Composer(network=client.network.string()) + account_config = StandardAccountBroadcasterConfig(private_key=private_key) + fee_calculator = MessageBasedTransactionFeeCalculator( + client=client, + composer=composer + ) + instance = cls( + network=network, + account_config=account_config, + client=client, + composer=composer, + fee_calculator=fee_calculator, + ) + return instance + @classmethod def new_for_grantee_account_using_simulation( cls, @@ -111,6 +137,31 @@ def new_for_grantee_account_using_simulation( ) return instance + @classmethod + def new_for_grantee_account_without_simulation( + cls, + network: Network, + grantee_private_key: str, + use_secure_connection: bool = True, + client: Optional[AsyncClient] = None, + composer: Optional[Composer] = None, + ): + client = client or AsyncClient(network=network, insecure=not (use_secure_connection)) + composer = composer or Composer(network=client.network.string()) + account_config = GranteeAccountBroadcasterConfig(grantee_private_key=grantee_private_key, composer=composer) + fee_calculator = MessageBasedTransactionFeeCalculator( + client=client, + composer=composer + ) + instance = cls( + network=network, + account_config=account_config, + client=client, + composer=composer, + fee_calculator=fee_calculator, + ) + return instance + async def broadcast(self, messages: List[any_pb2.Any]): await self._client.sync_timeout_height() await self._client.get_account(self._account_config.trading_injective_address) @@ -196,10 +247,16 @@ def messages_prepared_for_transaction(self, messages: List[any_pb2.Any]) -> List class SimulatedTransactionFeeCalculator(TransactionFeeCalculator): - def __init__(self, client: AsyncClient, composer: Composer, gas_price: Optional[int] = None): + def __init__( + self, + client: AsyncClient, + composer: Composer, + gas_price: Optional[int] = None, + gas_limit_adjustment_multiplier: Optional[Decimal] = None): self._client = client self._composer = composer self._gas_price = gas_price or self.DEFAULT_GAS_PRICE + self._gas_limit_adjustment_multiplier = gas_limit_adjustment_multiplier or Decimal("1.3") async def configure_gas_fee_for_transaction( self, @@ -216,7 +273,7 @@ async def configure_gas_fee_for_transaction( if not success: raise RuntimeError(f"Transaction simulation error: {sim_res}") - gas_limit = math.ceil(Decimal(str(sim_res.gas_info.gas_used)) * Decimal("1.1")) + gas_limit = math.ceil(Decimal(str(sim_res.gas_info.gas_used)) * self._gas_limit_adjustment_multiplier) fee = [ self._composer.Coin( @@ -227,3 +284,95 @@ async def configure_gas_fee_for_transaction( transaction.with_gas(gas=gas_limit) transaction.with_fee(fee=fee) + + +class MessageBasedTransactionFeeCalculator(TransactionFeeCalculator): + DEFAULT_GAS_LIMIT = 400_000 + DEFAULT_EXCHANGE_GAS_LIMIT = 200_000 + + def __init__( + self, + client: AsyncClient, + composer: Composer, + gas_price: Optional[int] = None, + base_gas_limit: Optional[int] = None, + base_exchange_gas_limit: Optional[int] = None): + self._client = client + self._composer = composer + self._gas_price = gas_price or self.DEFAULT_GAS_PRICE + self._base_gas_limit = base_gas_limit or self.DEFAULT_GAS_LIMIT + self._base_exchange_gas_limit = base_exchange_gas_limit or self.DEFAULT_EXCHANGE_GAS_LIMIT + + self._gas_limit_per_message_type_rules: List[Tuple[Callable, Decimal]] = [ + ( + lambda message: "MsgPrivilegedExecuteContract" in self._message_type(message=message), + Decimal("6") * self._base_gas_limit + ), + ( + lambda message: "MsgExecuteContract" in self._message_type(message=message), + Decimal("2.5") * self._base_gas_limit + ), + ( + lambda message: "wasm" in self._message_type(message=message), + Decimal("1.5") * self._base_gas_limit + ), + ( + lambda message: "exchange" in self._message_type(message=message), + Decimal("1") * self._base_exchange_gas_limit + ), + ( + lambda message: self._is_governance_message(message=message), + Decimal("15") * self._base_gas_limit + ), + ] + + async def configure_gas_fee_for_transaction( + self, + transaction: Transaction, + private_key: PrivateKey, + public_key: PublicKey, + ): + transaction_gas_limit = math.ceil(self._calculate_gas_limit(messages=transaction.msgs)) + + fee = [ + self._composer.Coin( + amount=math.ceil(self._gas_price * transaction_gas_limit), + denom=self._client.network.fee_denom, + ) + ] + + transaction.with_gas(gas=transaction_gas_limit) + transaction.with_fee(fee=fee) + + def _message_type(self, message: any_pb2.Any) -> str: + if isinstance(message, any_pb2.Any): + message_type = message.type_url + else: + message_type = message.DESCRIPTOR.full_name + return message_type + + def _is_governance_message(self, message: any_pb2.Any) -> bool: + message_type = self._message_type(message=message) + return "gov" in message_type and ("MsgDeposit" in message_type or "MsgSubmitProposal" in message_type) + + def _calculate_gas_limit(self, messages: List[any_pb2.Any]) -> int: + total_gas_limit = Decimal("0") + + for message in messages: + applying_rule = next( + (rule_tuple for rule_tuple in self._gas_limit_per_message_type_rules + if rule_tuple[0](message)), + None + ) + + if applying_rule is None: + total_gas_limit += self._base_gas_limit + else: + total_gas_limit += applying_rule[1] + + if self._message_type(message=message).endswith("MsgExec"): + exec_message = cosmos_authz_tx_pb.MsgExec.FromString(message.value) + sub_messages_limit = self._calculate_gas_limit(messages=exec_message.msgs) + total_gas_limit += sub_messages_limit + + return math.ceil(total_gas_limit) diff --git a/tests/core/test_message_based_transaction_fee_calculator.py b/tests/core/test_message_based_transaction_fee_calculator.py new file mode 100644 index 00000000..14bfea81 --- /dev/null +++ b/tests/core/test_message_based_transaction_fee_calculator.py @@ -0,0 +1,231 @@ +from decimal import Decimal + +import math +import pytest + +from pyinjective import Transaction +from pyinjective.async_client import AsyncClient +from pyinjective.composer import Composer +from pyinjective.constant import Network +from pyinjective.core.broadcaster import MessageBasedTransactionFeeCalculator +from pyinjective.proto.cosmos.gov.v1beta1 import tx_pb2 as gov_tx_pb2 +from pyinjective.proto.cosmwasm.wasm.v1 import tx_pb2 as wasm_tx_pb2 +from pyinjective.proto.injective.exchange.v1beta1 import tx_pb2 + + +class TestMessageBasedTransactionFeeCalculator: + + @pytest.mark.asyncio + async def test_gas_fee_for_privileged_execute_contract_message(self): + network = Network.testnet(node="sentry") + client = AsyncClient(network=network, insecure=False) + composer = Composer(network=network.string()) + calculator = MessageBasedTransactionFeeCalculator( + client=client, + composer=composer, + gas_price=5_000_000, + base_gas_limit=400_000, + base_exchange_gas_limit=150_000, + ) + + message = tx_pb2.MsgPrivilegedExecuteContract() + transaction = Transaction() + transaction.with_messages(message) + + await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) + + expected_gas_limit = math.ceil(Decimal(6) * 400_000) + assert(expected_gas_limit == transaction.fee.gas_limit) + assert(str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) + + @pytest.mark.asyncio + async def test_gas_fee_for_execute_contract_message(self): + network = Network.testnet(node="sentry") + client = AsyncClient(network=network, insecure=False) + composer = Composer(network=network.string()) + calculator = MessageBasedTransactionFeeCalculator( + client=client, + composer=composer, + gas_price=5_000_000, + base_gas_limit=400_000, + base_exchange_gas_limit=150_000, + ) + + message = composer.MsgExecuteContract( + sender="", + contract="", + msg="", + ) + transaction = Transaction() + transaction.with_messages(message) + + await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) + + expected_gas_limit = math.ceil(Decimal(2.5) * 400_000) + assert (expected_gas_limit == transaction.fee.gas_limit) + assert (str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) + + @pytest.mark.asyncio + async def test_gas_fee_for_wasm_message(self): + network = Network.testnet(node="sentry") + client = AsyncClient(network=network, insecure=False) + composer = Composer(network=network.string()) + calculator = MessageBasedTransactionFeeCalculator( + client=client, + composer=composer, + gas_price=5_000_000, + base_gas_limit=400_000, + base_exchange_gas_limit=150_000, + ) + + message = wasm_tx_pb2.MsgInstantiateContract2() + transaction = Transaction() + transaction.with_messages(message) + + await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) + + expected_gas_limit = math.ceil(Decimal(1.5) * 400_000) + assert (expected_gas_limit == transaction.fee.gas_limit) + assert (str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) + + @pytest.mark.asyncio + async def test_gas_fee_for_governance_message(self): + network = Network.testnet(node="sentry") + client = AsyncClient(network=network, insecure=False) + composer = Composer(network=network.string()) + calculator = MessageBasedTransactionFeeCalculator( + client=client, + composer=composer, + gas_price=5_000_000, + base_gas_limit=400_000, + base_exchange_gas_limit=150_000, + ) + + message = gov_tx_pb2.MsgDeposit() + transaction = Transaction() + transaction.with_messages(message) + + await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) + + expected_gas_limit = math.ceil(Decimal(15) * 400_000) + assert (expected_gas_limit == transaction.fee.gas_limit) + assert (str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) + + @pytest.mark.asyncio + async def test_gas_fee_for_exchange_message(self): + network = Network.testnet(node="sentry") + client = AsyncClient(network=network, insecure=False) + composer = Composer(network=network.string()) + calculator = MessageBasedTransactionFeeCalculator( + client=client, + composer=composer, + gas_price=5_000_000, + base_gas_limit=400_000, + base_exchange_gas_limit=150_000, + ) + + message = composer.MsgCreateSpotLimitOrder( + sender="sender", + market_id="0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe", + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=7.523, + quantity=0.01, + is_buy=True, + is_po=False + ) + transaction = Transaction() + transaction.with_messages(message) + + await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) + + expected_gas_limit = math.ceil(Decimal(1) * 150_000) + assert (expected_gas_limit == transaction.fee.gas_limit) + assert (str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) + + @pytest.mark.asyncio + async def test_gas_fee_for_msg_exec_message(self): + network = Network.testnet(node="sentry") + client = AsyncClient(network=network, insecure=False) + composer = Composer(network=network.string()) + calculator = MessageBasedTransactionFeeCalculator( + client=client, + composer=composer, + gas_price=5_000_000, + base_gas_limit=400_000, + base_exchange_gas_limit=150_000, + ) + + inner_message = composer.MsgCreateSpotLimitOrder( + sender="sender", + market_id="0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe", + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=7.523, + quantity=0.01, + is_buy=True, + is_po=False + ) + message = composer.MsgExec( + grantee="grantee", + msgs=[inner_message] + ) + transaction = Transaction() + transaction.with_messages(message) + + await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) + + expected_inner_message_gas_limit = Decimal(1) * 150_000 + expected_exec_message_gas_limit = 400_000 + expected_gas_limit = math.ceil(expected_exec_message_gas_limit + expected_inner_message_gas_limit) + assert (expected_gas_limit == transaction.fee.gas_limit) + assert (str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) + + @pytest.mark.asyncio + async def test_gas_fee_for_two_messages_in_one_transaction(self): + network = Network.testnet(node="sentry") + client = AsyncClient(network=network, insecure=False) + composer = Composer(network=network.string()) + calculator = MessageBasedTransactionFeeCalculator( + client=client, + composer=composer, + gas_price=5_000_000, + base_gas_limit=400_000, + base_exchange_gas_limit=150_000, + ) + + inner_message = composer.MsgCreateSpotLimitOrder( + sender="sender", + market_id="0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe", + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=7.523, + quantity=0.01, + is_buy=True, + is_po=False + ) + message = composer.MsgExec( + grantee="grantee", + msgs=[inner_message] + ) + + send_message = composer.MsgSend( + from_address="address", + to_address='to_address', + amount=1, + denom='INJ' + ) + + transaction = Transaction() + transaction.with_messages(message, send_message) + + await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) + + expected_inner_message_gas_limit = Decimal(1) * 150_000 + expected_exec_message_gas_limit = 400_000 + expected_send_message_gas_limit = 400_000 + expected_gas_limit = math.ceil( + expected_exec_message_gas_limit + expected_inner_message_gas_limit + expected_send_message_gas_limit + ) + assert (expected_gas_limit == transaction.fee.gas_limit) + assert (str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) From d0d602cd6a178fa88ac1323dfb93dd65f8cdd6d9 Mon Sep 17 00:00:00 2001 From: abel Date: Fri, 18 Aug 2023 00:58:52 -0300 Subject: [PATCH 02/17] (fix) Change version number and update README file --- README.md | 5 ++++- setup.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b4669a3f..d5b19728 100644 --- a/README.md +++ b/README.md @@ -87,8 +87,11 @@ make tests ``` ### Changelogs +**0.7.2** +* Added a new gas limit calculation for the TransactionBroadcaster that estimates the value based on the messages in the transaction (without running the transaction simulation). + **0.7.1** -* Include implementation of the MessageBroadcaster, to simplify the transaction creation and broadcasting process. +* Include implementation of the TransactionBroadcaster, to simplify the transaction creation and broadcasting process. **0.7.0.6** * ADD SEI/USDT in metadata diff --git a/setup.py b/setup.py index 34a68556..8481af42 100755 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ EMAIL = "achilleas@injectivelabs.com" AUTHOR = "Injective Labs" REQUIRES_PYTHON = ">=3.9" -VERSION = "0.7.1" +VERSION = "0.7.2" REQUIRED = [ "aiohttp", From 07fb8e7d5c09dae8223c1b98fe86a6ff2db0bf5f Mon Sep 17 00:00:00 2001 From: abel Date: Fri, 18 Aug 2023 12:47:44 -0300 Subject: [PATCH 03/17] (fix) Changes to gas limit estimation to reduce a little bit gas consumption --- pyinjective/async_client.py | 44 ++++++++++++++------------------- pyinjective/core/broadcaster.py | 10 ++++---- 2 files changed, 24 insertions(+), 30 deletions(-) diff --git a/pyinjective/async_client.py b/pyinjective/async_client.py index 96e88db4..965d1549 100644 --- a/pyinjective/async_client.py +++ b/pyinjective/async_client.py @@ -302,7 +302,7 @@ async def get_account(self, address: str) -> Optional[account_pb2.EthAccount]: try: metadata = await self.load_cookie(type="chain") account_any = (await self.stubAuth.Account( - auth_query.QueryAccountRequest.__call__(address=address), metadata=metadata + auth_query.QueryAccountRequest(address=address), metadata=metadata )).account account = account_pb2.EthAccount() if account_any.Is(account.DESCRIPTOR): @@ -339,7 +339,7 @@ async def simulate_tx( try: req = tx_service.SimulateRequest(tx_bytes=tx_byte) metadata = await self.load_cookie(type="chain") - return await self.stubTx.Simulate.__call__(req, metadata=metadata), True + return await self.stubTx.Simulate(request=req, metadata=metadata), True except grpc.RpcError as err: return err, False @@ -348,7 +348,7 @@ async def send_tx_sync_mode(self, tx_byte: bytes) -> abci_type.TxResponse: tx_bytes=tx_byte, mode=tx_service.BroadcastMode.BROADCAST_MODE_SYNC ) metadata = await self.load_cookie(type="chain") - result = await self.stubTx.BroadcastTx.__call__(req, metadata=metadata) + result = await self.stubTx.BroadcastTx(request=req, metadata=metadata) return result.tx_response async def send_tx_async_mode(self, tx_byte: bytes) -> abci_type.TxResponse: @@ -356,7 +356,7 @@ async def send_tx_async_mode(self, tx_byte: bytes) -> abci_type.TxResponse: tx_bytes=tx_byte, mode=tx_service.BroadcastMode.BROADCAST_MODE_ASYNC ) metadata = await self.load_cookie(type="chain") - result = await self.stubTx.BroadcastTx.__call__(req, metadata=metadata) + result = await self.stubTx.BroadcastTx(request=req, metadata=metadata) return result.tx_response async def send_tx_block_mode(self, tx_byte: bytes) -> abci_type.TxResponse: @@ -364,7 +364,7 @@ async def send_tx_block_mode(self, tx_byte: bytes) -> abci_type.TxResponse: tx_bytes=tx_byte, mode=tx_service.BroadcastMode.BROADCAST_MODE_BLOCK ) metadata = await self.load_cookie(type="chain") - result = await self.stubTx.BroadcastTx.__call__(req, metadata=metadata) + result = await self.stubTx.BroadcastTx(request=req, metadata=metadata) return result.tx_response async def get_chain_id(self) -> str: @@ -632,7 +632,7 @@ async def stream_spot_markets(self, **kwargs): market_ids=kwargs.get("market_ids") ) metadata = await self.load_cookie(type="exchange") - return self.stubSpotExchange.StreamMarkets.__call__(req, metadata=metadata) + return self.stubSpotExchange.StreamMarkets(request=req, metadata=metadata) async def get_spot_orderbookV2(self, market_id: str): req = spot_exchange_rpc_pb.OrderbookV2Request(market_id=market_id) @@ -689,12 +689,12 @@ async def get_spot_trades(self, **kwargs): async def stream_spot_orderbook_snapshot(self, market_ids: List[str]): req = spot_exchange_rpc_pb.StreamOrderbookV2Request(market_ids=market_ids) metadata = await self.load_cookie(type="exchange") - return self.stubSpotExchange.StreamOrderbookV2.__call__(req, metadata=metadata) + return self.stubSpotExchange.StreamOrderbookV2(request=req, metadata=metadata) async def stream_spot_orderbook_update(self, market_ids: List[str]): req = spot_exchange_rpc_pb.StreamOrderbookUpdateRequest(market_ids=market_ids) metadata = await self.load_cookie(type="exchange") - return self.stubSpotExchange.StreamOrderbookUpdate.__call__(req, metadata=metadata) + return self.stubSpotExchange.StreamOrderbookUpdate(request=req, metadata=metadata) async def stream_spot_orders(self, market_id: str, **kwargs): req = spot_exchange_rpc_pb.StreamOrdersRequest( @@ -703,7 +703,7 @@ async def stream_spot_orders(self, market_id: str, **kwargs): subaccount_id=kwargs.get("subaccount_id"), ) metadata = await self.load_cookie(type="exchange") - return self.stubSpotExchange.StreamOrders.__call__(req, metadata=metadata) + return self.stubSpotExchange.StreamOrders(request=req, metadata=metadata) async def stream_historical_spot_orders(self, market_id: str, **kwargs): req = spot_exchange_rpc_pb.StreamOrdersHistoryRequest( @@ -715,7 +715,7 @@ async def stream_historical_spot_orders(self, market_id: str, **kwargs): execution_types=kwargs.get("execution_types") ) metadata = await self.load_cookie(type="exchange") - return self.stubSpotExchange.StreamOrdersHistory.__call__(req, metadata=metadata) + return self.stubSpotExchange.StreamOrdersHistory(request=req, metadata=metadata) async def stream_historical_derivative_orders(self, market_id: str, **kwargs): req = derivative_exchange_rpc_pb.StreamOrdersHistoryRequest( @@ -727,7 +727,7 @@ async def stream_historical_derivative_orders(self, market_id: str, **kwargs): execution_types=kwargs.get("execution_types") ) metadata = await self.load_cookie(type="exchange") - return self.stubDerivativeExchange.StreamOrdersHistory.__call__(req, metadata=metadata) + return self.stubDerivativeExchange.StreamOrdersHistory(request=req, metadata=metadata) async def stream_spot_trades(self, **kwargs): req = spot_exchange_rpc_pb.StreamTradesRequest( @@ -740,7 +740,7 @@ async def stream_spot_trades(self, **kwargs): execution_types=kwargs.get("execution_types"), ) metadata = await self.load_cookie(type="exchange") - return self.stubSpotExchange.StreamTrades.__call__(req, metadata=metadata) + return self.stubSpotExchange.StreamTrades(request=req, metadata=metadata) async def get_spot_subaccount_orders(self, subaccount_id: str, **kwargs): req = spot_exchange_rpc_pb.SubaccountOrdersListRequest( @@ -780,7 +780,7 @@ async def stream_derivative_markets(self, **kwargs): market_ids=kwargs.get("market_ids") ) metadata = await self.load_cookie(type="exchange") - return self.stubDerivativeExchange.StreamMarket.__call__(req, metadata=metadata) + return self.stubDerivativeExchange.StreamMarket(request=req, metadata=metadata) async def get_derivative_orderbook(self, market_id: str): req = derivative_exchange_rpc_pb.OrderbookV2Request(market_id=market_id) @@ -846,16 +846,12 @@ async def get_derivative_trades(self, **kwargs): async def stream_derivative_orderbook_snapshot(self, market_ids: List[str]): req = derivative_exchange_rpc_pb.StreamOrderbookV2Request(market_ids=market_ids) metadata = await self.load_cookie(type="exchange") - return self.stubDerivativeExchange.StreamOrderbookV2.__call__( - req, metadata=metadata - ) + return self.stubDerivativeExchange.StreamOrderbookV2(request=req, metadata=metadata) async def stream_derivative_orderbook_update(self, market_ids: List[str]): req = derivative_exchange_rpc_pb.StreamOrderbookUpdateRequest(market_ids=market_ids) metadata = await self.load_cookie(type="exchange") - return self.stubDerivativeExchange.StreamOrderbookUpdate.__call__( - req, metadata=metadata - ) + return self.stubDerivativeExchange.StreamOrderbookUpdate(request=req, metadata=metadata) async def stream_derivative_orders(self, market_id: str, **kwargs): req = derivative_exchange_rpc_pb.StreamOrdersRequest( @@ -864,7 +860,7 @@ async def stream_derivative_orders(self, market_id: str, **kwargs): subaccount_id=kwargs.get("subaccount_id"), ) metadata = await self.load_cookie(type="exchange") - return self.stubDerivativeExchange.StreamOrders.__call__(req, metadata=metadata) + return self.stubDerivativeExchange.StreamOrders(request=req, metadata=metadata) async def stream_derivative_trades(self, **kwargs): req = derivative_exchange_rpc_pb.StreamTradesRequest( @@ -879,7 +875,7 @@ async def stream_derivative_trades(self, **kwargs): execution_types=kwargs.get("execution_types"), ) metadata = await self.load_cookie(type="exchange") - return self.stubDerivativeExchange.StreamTrades.__call__(req, metadata=metadata) + return self.stubDerivativeExchange.StreamTrades(request=req, metadata=metadata) async def get_derivative_positions(self, **kwargs): req = derivative_exchange_rpc_pb.PositionsRequest( @@ -901,9 +897,7 @@ async def stream_derivative_positions(self, **kwargs): subaccount_ids=kwargs.get("subaccount_ids"), ) metadata = await self.load_cookie(type="exchange") - return self.stubDerivativeExchange.StreamPositions.__call__( - req, metadata=metadata - ) + return self.stubDerivativeExchange.StreamPositions(request=req, metadata=metadata) async def get_derivative_liquidable_positions(self, **kwargs): req = derivative_exchange_rpc_pb.LiquidablePositionsRequest( @@ -979,4 +973,4 @@ async def stream_account_portfolio(self, account_address: str, **kwargs): type=kwargs.get("type") ) metadata = await self.load_cookie(type="exchange") - return self.stubPortfolio.StreamAccountPortfolio.__call__(req, metadata=metadata) + return self.stubPortfolio.StreamAccountPortfolio(request=req, metadata=metadata) diff --git a/pyinjective/core/broadcaster.py b/pyinjective/core/broadcaster.py index fb9f7eed..55606b85 100644 --- a/pyinjective/core/broadcaster.py +++ b/pyinjective/core/broadcaster.py @@ -287,8 +287,8 @@ async def configure_gas_fee_for_transaction( class MessageBasedTransactionFeeCalculator(TransactionFeeCalculator): - DEFAULT_GAS_LIMIT = 400_000 - DEFAULT_EXCHANGE_GAS_LIMIT = 200_000 + DEFAULT_GAS_LIMIT = 150_000 + DEFAULT_EXCHANGE_GAS_LIMIT = 100_000 def __init__( self, @@ -313,11 +313,11 @@ def __init__( Decimal("2.5") * self._base_gas_limit ), ( - lambda message: "wasm" in self._message_type(message=message), + lambda message: "wasm." in self._message_type(message=message), Decimal("1.5") * self._base_gas_limit ), ( - lambda message: "exchange" in self._message_type(message=message), + lambda message: "exchange." in self._message_type(message=message), Decimal("1") * self._base_exchange_gas_limit ), ( @@ -353,7 +353,7 @@ def _message_type(self, message: any_pb2.Any) -> str: def _is_governance_message(self, message: any_pb2.Any) -> bool: message_type = self._message_type(message=message) - return "gov" in message_type and ("MsgDeposit" in message_type or "MsgSubmitProposal" in message_type) + return "gov." in message_type and ("MsgDeposit" in message_type or "MsgSubmitProposal" in message_type) def _calculate_gas_limit(self, messages: List[any_pb2.Any]) -> int: total_gas_limit = Decimal("0") From 8c2bf01effcaed5dc8cc8f59c4439cf44611071e Mon Sep 17 00:00:00 2001 From: abel Date: Fri, 18 Aug 2023 13:09:47 -0300 Subject: [PATCH 04/17] (fix) Undo change in the example to get a transaction by hash --- examples/exchange_client/explorer_rpc/1_GetTxByHash.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/exchange_client/explorer_rpc/1_GetTxByHash.py b/examples/exchange_client/explorer_rpc/1_GetTxByHash.py index fa21e293..7362d1e0 100644 --- a/examples/exchange_client/explorer_rpc/1_GetTxByHash.py +++ b/examples/exchange_client/explorer_rpc/1_GetTxByHash.py @@ -7,10 +7,10 @@ async def main() -> None: # select network: local, testnet, mainnet - network = Network.mainnet() + network = Network.testnet() client = AsyncClient(network, insecure=False) composer = Composer(network=network.string()) - tx_hash = "50DD80270052D85835939A393127B5626917007E71A7ABD5205F1A094B976C1B" + tx_hash = "0F3EBEC1882E1EEAC5B7BDD836E976250F1CD072B79485877CEACCB92ACDDF52" transaction_response = await client.get_tx_by_hash(tx_hash=tx_hash) print(transaction_response) From 0aa1c7bba33576fc69d61e636f96146f9956197a Mon Sep 17 00:00:00 2001 From: abel Date: Tue, 22 Aug 2023 10:20:04 -0300 Subject: [PATCH 05/17] (feat) Improved the logic in the gas limit estimator component to consider the cost of each included element for batch messages --- pyinjective/core/broadcaster.py | 60 +- pyinjective/core/gas_limit_estimator.py | 307 ++++++++++ tests/core/test_gas_limit_estimator.py | 527 ++++++++++++++++++ ...essage_based_transaction_fee_calculator.py | 50 +- 4 files changed, 866 insertions(+), 78 deletions(-) create mode 100644 pyinjective/core/gas_limit_estimator.py create mode 100644 tests/core/test_gas_limit_estimator.py diff --git a/pyinjective/core/broadcaster.py b/pyinjective/core/broadcaster.py index 55606b85..cc9dc2bb 100644 --- a/pyinjective/core/broadcaster.py +++ b/pyinjective/core/broadcaster.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod from decimal import Decimal -from typing import Callable, List, Optional, Tuple +from typing import List, Optional import math from google.protobuf import any_pb2 @@ -9,7 +9,7 @@ from pyinjective.async_client import AsyncClient from pyinjective.composer import Composer from pyinjective.constant import Network -from pyinjective.proto.cosmos.authz.v1beta1 import tx_pb2 as cosmos_authz_tx_pb +from pyinjective.core.gas_limit_estimator import GasLimitEstimator class BroadcasterAccountConfig(ABC): @@ -287,44 +287,16 @@ async def configure_gas_fee_for_transaction( class MessageBasedTransactionFeeCalculator(TransactionFeeCalculator): - DEFAULT_GAS_LIMIT = 150_000 - DEFAULT_EXCHANGE_GAS_LIMIT = 100_000 + TRANSACTION_GAS_LIMIT = 60_000 def __init__( self, client: AsyncClient, composer: Composer, - gas_price: Optional[int] = None, - base_gas_limit: Optional[int] = None, - base_exchange_gas_limit: Optional[int] = None): + gas_price: Optional[int] = None): self._client = client self._composer = composer self._gas_price = gas_price or self.DEFAULT_GAS_PRICE - self._base_gas_limit = base_gas_limit or self.DEFAULT_GAS_LIMIT - self._base_exchange_gas_limit = base_exchange_gas_limit or self.DEFAULT_EXCHANGE_GAS_LIMIT - - self._gas_limit_per_message_type_rules: List[Tuple[Callable, Decimal]] = [ - ( - lambda message: "MsgPrivilegedExecuteContract" in self._message_type(message=message), - Decimal("6") * self._base_gas_limit - ), - ( - lambda message: "MsgExecuteContract" in self._message_type(message=message), - Decimal("2.5") * self._base_gas_limit - ), - ( - lambda message: "wasm." in self._message_type(message=message), - Decimal("1.5") * self._base_gas_limit - ), - ( - lambda message: "exchange." in self._message_type(message=message), - Decimal("1") * self._base_exchange_gas_limit - ), - ( - lambda message: self._is_governance_message(message=message), - Decimal("15") * self._base_gas_limit - ), - ] async def configure_gas_fee_for_transaction( self, @@ -332,7 +304,8 @@ async def configure_gas_fee_for_transaction( private_key: PrivateKey, public_key: PublicKey, ): - transaction_gas_limit = math.ceil(self._calculate_gas_limit(messages=transaction.msgs)) + messages_gas_limit = math.ceil(self._calculate_gas_limit(messages=transaction.msgs)) + transaction_gas_limit = messages_gas_limit + self.TRANSACTION_GAS_LIMIT fee = [ self._composer.Coin( @@ -351,28 +324,11 @@ def _message_type(self, message: any_pb2.Any) -> str: message_type = message.DESCRIPTOR.full_name return message_type - def _is_governance_message(self, message: any_pb2.Any) -> bool: - message_type = self._message_type(message=message) - return "gov." in message_type and ("MsgDeposit" in message_type or "MsgSubmitProposal" in message_type) - def _calculate_gas_limit(self, messages: List[any_pb2.Any]) -> int: total_gas_limit = Decimal("0") for message in messages: - applying_rule = next( - (rule_tuple for rule_tuple in self._gas_limit_per_message_type_rules - if rule_tuple[0](message)), - None - ) - - if applying_rule is None: - total_gas_limit += self._base_gas_limit - else: - total_gas_limit += applying_rule[1] - - if self._message_type(message=message).endswith("MsgExec"): - exec_message = cosmos_authz_tx_pb.MsgExec.FromString(message.value) - sub_messages_limit = self._calculate_gas_limit(messages=exec_message.msgs) - total_gas_limit += sub_messages_limit + estimator = GasLimitEstimator.for_message(message=message) + total_gas_limit += estimator.gas_limit() return math.ceil(total_gas_limit) diff --git a/pyinjective/core/gas_limit_estimator.py b/pyinjective/core/gas_limit_estimator.py new file mode 100644 index 00000000..d21c385b --- /dev/null +++ b/pyinjective/core/gas_limit_estimator.py @@ -0,0 +1,307 @@ +from abc import ABC, abstractmethod + +from google.protobuf import any_pb2 + +from pyinjective.proto.cosmos.authz.v1beta1 import tx_pb2 as cosmos_authz_tx_pb +from pyinjective.proto.cosmos.gov.v1beta1 import tx_pb2 as gov_tx_pb +from pyinjective.proto.cosmwasm.wasm.v1 import tx_pb2 as wasm_tx_pb +from pyinjective.proto.injective.exchange.v1beta1 import tx_pb2 as injective_exchange_tx_pb + + +class GasLimitEstimator(ABC): + GENERAL_MESSAGE_GAS_LIMIT = 5_000 + BASIC_REFERENCE_GAS_LIMIT = 150_000 + + @classmethod + @abstractmethod + def applies_to(cls, message: any_pb2.Any) -> bool: + ... + + @classmethod + def for_message(cls, message: any_pb2.Any): + estimator_class = next((estimator_subclass for estimator_subclass in cls.__subclasses__() + if estimator_subclass.applies_to(message=message)), + None) + if estimator_class is None: + estimator = DefaultGasLimitEstimator() + else: + estimator = estimator_class(message=message) + + return estimator + + @abstractmethod + def gas_limit(self) -> int: + ... + + @staticmethod + def message_type(message: any_pb2.Any) -> str: + if isinstance(message, any_pb2.Any): + message_type = message.type_url + else: + message_type = message.DESCRIPTOR.full_name + return message_type + + @abstractmethod + def _message_class(self, message: any_pb2.Any): + ... + + def _parsed_message(self, message: any_pb2.Any) -> any_pb2.Any: + if isinstance(message, any_pb2.Any): + parsed_message = self._message_class(message=message).FromString(message.value) + else: + parsed_message = message + return parsed_message + + +class DefaultGasLimitEstimator(GasLimitEstimator): + DEFAULT_GAS_LIMIT = 150_000 + + @classmethod + def applies_to(cls, message: any_pb2.Any) -> bool: + return False + + def gas_limit(self) -> int: + return self.DEFAULT_GAS_LIMIT + + def _message_class(self, message: any_pb2.Any): + # This class should not try to convert messages + raise NotImplementedError + + +class BatchCreateSpotLimitOrdersGasLimitEstimator(GasLimitEstimator): + ORDER_GAS_LIMIT = 45_000 + + def __init__(self, message: any_pb2.Any): + self._message = self._parsed_message(message=message) + + @classmethod + def applies_to(cls, message: any_pb2.Any): + return cls.message_type(message=message).endswith("MsgBatchCreateSpotLimitOrders") + + def gas_limit(self) -> int: + total = 0 + total += self.GENERAL_MESSAGE_GAS_LIMIT + total += len(self._message.orders) * self.ORDER_GAS_LIMIT + + return total + + def _message_class(self, message: any_pb2.Any): + return injective_exchange_tx_pb.MsgBatchCreateSpotLimitOrders + + +class BatchCancelSpotOrdersGasLimitEstimator(GasLimitEstimator): + ORDER_GAS_LIMIT = 45_000 + + def __init__(self, message: any_pb2.Any): + self._message = self._parsed_message(message=message) + + @classmethod + def applies_to(cls, message: any_pb2.Any): + return cls.message_type(message=message).endswith("MsgBatchCancelSpotOrders") + + def gas_limit(self) -> int: + total = 0 + total += self.GENERAL_MESSAGE_GAS_LIMIT + total += len(self._message.data) * self.ORDER_GAS_LIMIT + + return total + + def _message_class(self, message: any_pb2.Any): + return injective_exchange_tx_pb.MsgBatchCancelSpotOrders + + +class BatchCreateDerivativeLimitOrdersGasLimitEstimator(GasLimitEstimator): + ORDER_GAS_LIMIT = 60_000 + + def __init__(self, message: any_pb2.Any): + self._message = self._parsed_message(message=message) + + @classmethod + def applies_to(cls, message: any_pb2.Any): + return cls.message_type(message=message).endswith("MsgBatchCreateDerivativeLimitOrders") + + def gas_limit(self) -> int: + total = 0 + total += self.GENERAL_MESSAGE_GAS_LIMIT + total += len(self._message.orders) * self.ORDER_GAS_LIMIT + + return total + + def _message_class(self, message: any_pb2.Any): + return injective_exchange_tx_pb.MsgBatchCreateDerivativeLimitOrders + + +class BatchCancelDerivativeOrdersGasLimitEstimator(GasLimitEstimator): + ORDER_GAS_LIMIT = 55_000 + + def __init__(self, message: any_pb2.Any): + self._message = self._parsed_message(message=message) + + @classmethod + def applies_to(cls, message: any_pb2.Any): + return cls.message_type(message=message).endswith("MsgBatchCancelDerivativeOrders") + + def gas_limit(self) -> int: + total = 0 + total += self.GENERAL_MESSAGE_GAS_LIMIT + total += len(self._message.data) * self.ORDER_GAS_LIMIT + + return total + + def _message_class(self, message: any_pb2.Any): + return injective_exchange_tx_pb.MsgBatchCancelDerivativeOrders + + +class BatchUpdateOrdersGasLimitEstimator(GasLimitEstimator): + SPOT_ORDER_CREATION_GAS_LIMIT = 40_000 + DERIVATIVE_ORDER_CREATION_GAS_LIMIT = 60_000 + SPOT_ORDER_CANCELATION_GAS_LIMIT = 45_000 + DERIVATIVE_ORDER_CANCELATION_GAS_LIMIT = 55_000 + CANCEL_ALL_SPOT_MARKET_GAS_LIMIT = 35_000 + CANCEL_ALL_DERIVATIVE_MARKET_GAS_LIMIT = 45_000 + MESSAGE_GAS_LIMIT = 10_000 + + AVERAGE_CANCEL_ALL_AFFECTED_ORDERS = 20 + + def __init__(self, message: any_pb2.Any): + self._message = self._parsed_message(message=message) + + @classmethod + def applies_to(cls, message: any_pb2.Any): + return cls.message_type(message=message).endswith("MsgBatchUpdateOrders") + + def gas_limit(self) -> int: + total = 0 + total += self.MESSAGE_GAS_LIMIT + total += len(self._message.spot_orders_to_create) * self.SPOT_ORDER_CREATION_GAS_LIMIT + total += len(self._message.derivative_orders_to_create) * self.DERIVATIVE_ORDER_CREATION_GAS_LIMIT + total += len(self._message.binary_options_orders_to_create) * self.DERIVATIVE_ORDER_CREATION_GAS_LIMIT + total += len(self._message.spot_orders_to_cancel) * self.SPOT_ORDER_CANCELATION_GAS_LIMIT + total += len(self._message.derivative_orders_to_cancel) * self.DERIVATIVE_ORDER_CANCELATION_GAS_LIMIT + total += len(self._message.binary_options_orders_to_cancel) * self.DERIVATIVE_ORDER_CANCELATION_GAS_LIMIT + + total += (len(self._message.spot_market_ids_to_cancel_all) + * self.CANCEL_ALL_SPOT_MARKET_GAS_LIMIT + * self.AVERAGE_CANCEL_ALL_AFFECTED_ORDERS) + total += (len(self._message.derivative_market_ids_to_cancel_all) + * self.CANCEL_ALL_DERIVATIVE_MARKET_GAS_LIMIT + * self.AVERAGE_CANCEL_ALL_AFFECTED_ORDERS) + total += (len(self._message.binary_options_market_ids_to_cancel_all) + * self.CANCEL_ALL_DERIVATIVE_MARKET_GAS_LIMIT + * self.AVERAGE_CANCEL_ALL_AFFECTED_ORDERS) + + return total + + def _message_class(self, message: any_pb2.Any): + return injective_exchange_tx_pb.MsgBatchUpdateOrders + + +class ExecGasLimitEstimator(GasLimitEstimator): + DEFAULT_GAS_LIMIT = 5_000 + + def __init__(self, message: any_pb2.Any): + self._message = self._parsed_message(message=message) + + @classmethod + def applies_to(cls, message: any_pb2.Any) -> bool: + return cls.message_type(message=message).endswith("MsgExec") + + def gas_limit(self) -> int: + total = sum([GasLimitEstimator.for_message(message=inner_message).gas_limit() + for inner_message in self._message.msgs]) + total += self.DEFAULT_GAS_LIMIT + + return total + + def _message_class(self, message: any_pb2.Any): + return cosmos_authz_tx_pb.MsgExec + + +class PrivilegedExecuteContractGasLimitEstimator(GasLimitEstimator): + + def __init__(self, message: any_pb2.Any): + self._message = self._parsed_message(message=message) + + @classmethod + def applies_to(cls, message: any_pb2.Any) -> bool: + return cls.message_type(message=message).endswith("MsgPrivilegedExecuteContract") + + def gas_limit(self) -> int: + return self.BASIC_REFERENCE_GAS_LIMIT * 6 + + def _message_class(self, message: any_pb2.Any): + return injective_exchange_tx_pb.MsgPrivilegedExecuteContract + + +class ExecuteContractGasLimitEstimator(GasLimitEstimator): + + def __init__(self, message: any_pb2.Any): + self._message = self._parsed_message(message=message) + + @classmethod + def applies_to(cls, message: any_pb2.Any) -> bool: + return cls.message_type(message=message).endswith("MsgExecuteContract") + + def gas_limit(self) -> int: + return int(self.BASIC_REFERENCE_GAS_LIMIT * 2.5) + + def _message_class(self, message: any_pb2.Any): + return wasm_tx_pb.MsgExecuteContract + + +class GeneralWasmGasLimitEstimator(GasLimitEstimator): + + def __init__(self, message: any_pb2.Any): + self._message = self._parsed_message(message=message) + + @classmethod + def applies_to(cls, message: any_pb2.Any) -> bool: + return "wasm." in cls.message_type(message=message) + + def gas_limit(self) -> int: + return int(self.BASIC_REFERENCE_GAS_LIMIT * 1.5) + + def _message_class(self, message: any_pb2.Any): + return wasm_tx_pb.MsgInstantiateContract2 + + +class GovernanceGasLimitEstimator(GasLimitEstimator): + + def __init__(self, message: any_pb2.Any): + self._message = self._parsed_message(message=message) + + @classmethod + def applies_to(cls, message: any_pb2.Any) -> bool: + message_type = cls.message_type(message=message) + return "gov." in message_type and ( + message_type.endswith("MsgDeposit") or message_type.endswith("MsgSubmitProposal") + ) + + def gas_limit(self) -> int: + return int(self.BASIC_REFERENCE_GAS_LIMIT * 15) + + def _message_class(self, message: any_pb2.Any): + if "MsgDeposit" in self.message_type(message=message): + message_class = gov_tx_pb.MsgDeposit + else: + message_class = gov_tx_pb.MsgSubmitProposal + return message_class + + +class GenericExchangeGasLimitEstimator(GasLimitEstimator): + BASIC_REFERENCE_GAS_LIMIT = 100_000 + + def __init__(self, message: any_pb2.Any): + self._message = message + + @classmethod + def applies_to(cls, message: any_pb2.Any) -> bool: + message_type = cls.message_type(message=message) + return "exchange." in message_type + + def gas_limit(self) -> int: + return self.BASIC_REFERENCE_GAS_LIMIT + + def _message_class(self, message: any_pb2.Any): + # This class applies to many different messages, but we don't need to transform from Any format + raise NotImplementedError diff --git a/tests/core/test_gas_limit_estimator.py b/tests/core/test_gas_limit_estimator.py new file mode 100644 index 00000000..070ebb26 --- /dev/null +++ b/tests/core/test_gas_limit_estimator.py @@ -0,0 +1,527 @@ +from pyinjective.composer import Composer +from pyinjective.core.gas_limit_estimator import GasLimitEstimator +from pyinjective.proto.cosmos.gov.v1beta1 import tx_pb2 as gov_tx_pb +from pyinjective.proto.cosmwasm.wasm.v1 import tx_pb2 as wasm_tx_pb +from pyinjective.proto.injective.exchange.v1beta1 import tx_pb2 as injective_exchange_tx_pb + + +class TestGasLimitEstimator: + + def test_estimation_for_message_without_applying_rule(self): + composer = Composer(network="testnet") + message = composer.MsgSend( + from_address="from_address", + to_address="to_address", + amount=1, + denom='INJ' + ) + + estimator = GasLimitEstimator.for_message(message=message) + + expected_message_gas_limit = 150_000 + + assert(expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_batch_create_spot_limit_orders(self): + spot_market_id = "0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe" + composer = Composer(network="testnet") + orders = [ + composer.SpotOrder( + market_id=spot_market_id, + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=5, + quantity=1, + is_buy=True, + is_po=False + ), + composer.SpotOrder( + market_id=spot_market_id, + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=4, + quantity=1, + is_buy=True, + is_po=False + ), + ] + message = composer.MsgBatchCreateSpotLimitOrders( + sender="sender", + orders=orders + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_order_gas_limit = 45000 + expected_message_gas_limit = 5000 + + assert((expected_order_gas_limit * 2) + expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_batch_cancel_spot_orders(self): + spot_market_id = "0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe" + composer = Composer(network="testnet") + orders = [ + composer.OrderData( + market_id=spot_market_id, + subaccount_id="subaccount_id", + order_hash="0x3870fbdd91f07d54425147b1bb96404f4f043ba6335b422a6d494d285b387f2d" + ), + composer.OrderData( + market_id=spot_market_id, + subaccount_id="subaccount_id", + order_hash="0x222daa22f60fe9f075ed0ca583459e121c23e64431c3fbffdedda04598ede0d2" + ), + composer.OrderData( + market_id=spot_market_id, + subaccount_id="subaccount_id", + order_hash="0x7ee76255d7ca763c56b0eab9828fca89fdd3739645501c8a80f58b62b4f76da5" + ) + ] + message = composer.MsgBatchCancelSpotOrders( + sender="sender", + data=orders + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_order_gas_limit = 45000 + expected_message_gas_limit = 5000 + + assert((expected_order_gas_limit * 3) + expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_batch_create_derivative_limit_orders(self): + market_id = "0x17ef48032cb24375ba7c2e39f384e56433bcab20cbee9a7357e4cba2eb00abe6" + composer = Composer(network="testnet") + orders = [ + composer.DerivativeOrder( + market_id=market_id, + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=3, + quantity=1, + leverage=1, + is_buy=True, + is_po=False + ), + composer.DerivativeOrder( + market_id=market_id, + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=20, + quantity=1, + leverage=1, + is_buy=False, + is_reduce_only=False + ), + ] + message = composer.MsgBatchCreateDerivativeLimitOrders( + sender="sender", + orders=orders + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_order_gas_limit = 60_000 + expected_message_gas_limit = 5000 + + assert((expected_order_gas_limit * 2) + expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_batch_cancel_derivative_orders(self): + spot_market_id = "0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe" + composer = Composer(network="testnet") + orders = [ + composer.OrderData( + market_id=spot_market_id, + subaccount_id="subaccount_id", + order_hash="0x3870fbdd91f07d54425147b1bb96404f4f043ba6335b422a6d494d285b387f2d" + ), + composer.OrderData( + market_id=spot_market_id, + subaccount_id="subaccount_id", + order_hash="0x222daa22f60fe9f075ed0ca583459e121c23e64431c3fbffdedda04598ede0d2" + ), + composer.OrderData( + market_id=spot_market_id, + subaccount_id="subaccount_id", + order_hash="0x7ee76255d7ca763c56b0eab9828fca89fdd3739645501c8a80f58b62b4f76da5" + ) + ] + message = composer.MsgBatchCancelDerivativeOrders( + sender="sender", + data=orders + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_order_gas_limit = 55_000 + expected_message_gas_limit = 5000 + + assert((expected_order_gas_limit * 3) + expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_batch_update_orders_to_create_spot_orders(self): + market_id = "0x17ef48032cb24375ba7c2e39f384e56433bcab20cbee9a7357e4cba2eb00abe6" + composer = Composer(network="testnet") + orders = [ + composer.SpotOrder( + market_id=market_id, + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=5, + quantity=1, + is_buy=True, + is_po=False + ), + composer.SpotOrder( + market_id=market_id, + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=4, + quantity=1, + is_buy=True, + is_po=False + ), + ] + message = composer.MsgBatchUpdateOrders( + sender="senders", + derivative_orders_to_create=[], + spot_orders_to_create=orders, + derivative_orders_to_cancel=[], + spot_orders_to_cancel=[] + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_order_gas_limit = 40_000 + expected_message_gas_limit = 10_000 + + assert((expected_order_gas_limit * 2) + expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_batch_update_orders_to_create_derivative_orders(self): + market_id = "0x17ef48032cb24375ba7c2e39f384e56433bcab20cbee9a7357e4cba2eb00abe6" + composer = Composer(network="testnet") + orders = [ + composer.DerivativeOrder( + market_id=market_id, + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=3, + quantity=1, + leverage=1, + is_buy=True, + is_po=False + ), + composer.DerivativeOrder( + market_id=market_id, + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=20, + quantity=1, + leverage=1, + is_buy=False, + is_reduce_only=False + ), + ] + message = composer.MsgBatchUpdateOrders( + sender="senders", + derivative_orders_to_create=orders, + spot_orders_to_create=[], + derivative_orders_to_cancel=[], + spot_orders_to_cancel=[] + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_order_gas_limit = 60_000 + expected_message_gas_limit = 10_000 + + assert((expected_order_gas_limit * 2) + expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_batch_update_orders_to_create_binary_orders(self): + market_id = "0x17ef48032cb24375ba7c2e39f384e56433bcab20cbee9a7357e4cba2eb00abe6" + composer = Composer(network="testnet") + orders = [ + composer.BinaryOptionsOrder( + market_id=market_id, + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=3, + quantity=1, + leverage=1, + is_buy=True, + is_po=False + ), + composer.BinaryOptionsOrder( + market_id=market_id, + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=20, + quantity=1, + leverage=1, + is_buy=False, + is_reduce_only=False + ), + ] + message = composer.MsgBatchUpdateOrders( + sender="senders", + derivative_orders_to_create=[], + spot_orders_to_create=[], + binary_options_orders_to_create=orders, + derivative_orders_to_cancel=[], + spot_orders_to_cancel=[] + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_order_gas_limit = 60_000 + expected_message_gas_limit = 10_000 + + assert((expected_order_gas_limit * 2) + expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_batch_update_orders_to_cancel_spot_orders(self): + market_id = "0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe" + composer = Composer(network="testnet") + orders = [ + composer.OrderData( + market_id=market_id, + subaccount_id="subaccount_id", + order_hash="0x3870fbdd91f07d54425147b1bb96404f4f043ba6335b422a6d494d285b387f2d" + ), + composer.OrderData( + market_id=market_id, + subaccount_id="subaccount_id", + order_hash="0x222daa22f60fe9f075ed0ca583459e121c23e64431c3fbffdedda04598ede0d2" + ), + composer.OrderData( + market_id=market_id, + subaccount_id="subaccount_id", + order_hash="0x7ee76255d7ca763c56b0eab9828fca89fdd3739645501c8a80f58b62b4f76da5" + ) + ] + message = composer.MsgBatchUpdateOrders( + sender="senders", + derivative_orders_to_create=[], + spot_orders_to_create=[], + derivative_orders_to_cancel=[], + spot_orders_to_cancel=orders + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_order_gas_limit = 45_000 + expected_message_gas_limit = 10_000 + + assert((expected_order_gas_limit * 3) + expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_batch_update_orders_to_cancel_derivative_orders(self): + market_id = "0x17ef48032cb24375ba7c2e39f384e56433bcab20cbee9a7357e4cba2eb00abe6" + composer = Composer(network="testnet") + orders = [ + composer.OrderData( + market_id=market_id, + subaccount_id="subaccount_id", + order_hash="0x3870fbdd91f07d54425147b1bb96404f4f043ba6335b422a6d494d285b387f2d" + ), + composer.OrderData( + market_id=market_id, + subaccount_id="subaccount_id", + order_hash="0x222daa22f60fe9f075ed0ca583459e121c23e64431c3fbffdedda04598ede0d2" + ), + composer.OrderData( + market_id=market_id, + subaccount_id="subaccount_id", + order_hash="0x7ee76255d7ca763c56b0eab9828fca89fdd3739645501c8a80f58b62b4f76da5" + ) + ] + message = composer.MsgBatchUpdateOrders( + sender="senders", + derivative_orders_to_create=[], + spot_orders_to_create=[], + derivative_orders_to_cancel=orders, + spot_orders_to_cancel=[] + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_order_gas_limit = 55_000 + expected_message_gas_limit = 10_000 + + assert((expected_order_gas_limit * 3) + expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_batch_update_orders_to_cancel_binary_orders(self): + market_id = "0x17ef48032cb24375ba7c2e39f384e56433bcab20cbee9a7357e4cba2eb00abe6" + composer = Composer(network="testnet") + orders = [ + composer.OrderData( + market_id=market_id, + subaccount_id="subaccount_id", + order_hash="0x3870fbdd91f07d54425147b1bb96404f4f043ba6335b422a6d494d285b387f2d" + ), + composer.OrderData( + market_id=market_id, + subaccount_id="subaccount_id", + order_hash="0x222daa22f60fe9f075ed0ca583459e121c23e64431c3fbffdedda04598ede0d2" + ), + composer.OrderData( + market_id=market_id, + subaccount_id="subaccount_id", + order_hash="0x7ee76255d7ca763c56b0eab9828fca89fdd3739645501c8a80f58b62b4f76da5" + ) + ] + message = composer.MsgBatchUpdateOrders( + sender="senders", + derivative_orders_to_create=[], + spot_orders_to_create=[], + derivative_orders_to_cancel=[], + spot_orders_to_cancel=[], + binary_options_orders_to_cancel=orders, + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_order_gas_limit = 55_000 + expected_message_gas_limit = 10_000 + + assert((expected_order_gas_limit * 3) + expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_batch_update_orders_to_cancel_all_for_spot_market(self): + market_id = "0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe" + composer = Composer(network="testnet") + + message = composer.MsgBatchUpdateOrders( + sender="senders", + subaccount_id="subaccount_id", + spot_market_ids_to_cancel_all=[market_id], + derivative_orders_to_create=[], + spot_orders_to_create=[], + derivative_orders_to_cancel=[], + spot_orders_to_cancel=[] + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_gas_limit = 35_000 * 20 + expected_message_gas_limit = 10_000 + + assert(expected_gas_limit + expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_batch_update_orders_to_cancel_all_for_derivative_market(self): + market_id = "0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe" + composer = Composer(network="testnet") + + message = composer.MsgBatchUpdateOrders( + sender="senders", + subaccount_id="subaccount_id", + derivative_market_ids_to_cancel_all=[market_id], + derivative_orders_to_create=[], + spot_orders_to_create=[], + derivative_orders_to_cancel=[], + spot_orders_to_cancel=[] + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_gas_limit = 45_000 * 20 + expected_message_gas_limit = 10_000 + + assert(expected_gas_limit + expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_batch_update_orders_to_cancel_all_for_binary_options_market(self): + market_id = "0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe" + composer = Composer(network="testnet") + + message = composer.MsgBatchUpdateOrders( + sender="senders", + subaccount_id="subaccount_id", + binary_options_market_ids_to_cancel_all=[market_id], + derivative_orders_to_create=[], + spot_orders_to_create=[], + derivative_orders_to_cancel=[], + spot_orders_to_cancel=[] + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_gas_limit = 45_000 * 20 + expected_message_gas_limit = 10_000 + + assert(expected_gas_limit + expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_exec_message(self): + market_id = "0x17ef48032cb24375ba7c2e39f384e56433bcab20cbee9a7357e4cba2eb00abe6" + composer = Composer(network="testnet") + orders = [ + composer.SpotOrder( + market_id=market_id, + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=5, + quantity=1, + is_buy=True, + is_po=False + ), + ] + inner_message = composer.MsgBatchUpdateOrders( + sender="senders", + derivative_orders_to_create=[], + spot_orders_to_create=orders, + derivative_orders_to_cancel=[], + spot_orders_to_cancel=[] + ) + message = composer.MsgExec( + grantee="grantee", + msgs=[inner_message] + ) + + estimator = GasLimitEstimator.for_message(message=message) + + expected_order_gas_limit = 40_000 + expected_inner_message_gas_limit = 10_000 + expected_exec_message_gas_limit = 5_000 + + assert(expected_order_gas_limit + + expected_inner_message_gas_limit + + expected_exec_message_gas_limit + == estimator.gas_limit()) + + def test_estimation_for_privileged_execute_contract_message(self): + message = injective_exchange_tx_pb.MsgPrivilegedExecuteContract() + estimator = GasLimitEstimator.for_message(message=message) + + expected_gas_limit = 900_000 + + assert(expected_gas_limit == estimator.gas_limit()) + + + def test_estimation_for_execute_contract_message(self): + composer = Composer(network="testnet") + message = composer.MsgExecuteContract( + sender="", + contract="", + msg="", + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_gas_limit = 375_000 + + assert(expected_gas_limit == estimator.gas_limit()) + + def test_estimation_for_wasm_message(self): + message = wasm_tx_pb.MsgInstantiateContract2() + estimator = GasLimitEstimator.for_message(message=message) + + expected_gas_limit = 225_000 + + assert(expected_gas_limit == estimator.gas_limit()) + + def test_estimation_for_governance_message(self): + message = gov_tx_pb.MsgDeposit() + estimator = GasLimitEstimator.for_message(message=message) + + expected_gas_limit = 2_250_000 + + assert(expected_gas_limit == estimator.gas_limit()) + + def test_estimation_for_generic_exchange_message(self): + composer = Composer(network="testnet") + message = composer.MsgCreateSpotLimitOrder( + sender="sender", + market_id="0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe", + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=7.523, + quantity=0.01, + is_buy=True, + is_po=False + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_gas_limit = 100_000 + + assert(expected_gas_limit == estimator.gas_limit()) diff --git a/tests/core/test_message_based_transaction_fee_calculator.py b/tests/core/test_message_based_transaction_fee_calculator.py index 14bfea81..602b71af 100644 --- a/tests/core/test_message_based_transaction_fee_calculator.py +++ b/tests/core/test_message_based_transaction_fee_calculator.py @@ -24,8 +24,6 @@ async def test_gas_fee_for_privileged_execute_contract_message(self): client=client, composer=composer, gas_price=5_000_000, - base_gas_limit=400_000, - base_exchange_gas_limit=150_000, ) message = tx_pb2.MsgPrivilegedExecuteContract() @@ -34,7 +32,8 @@ async def test_gas_fee_for_privileged_execute_contract_message(self): await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) - expected_gas_limit = math.ceil(Decimal(6) * 400_000) + expected_transaction_gas_limit = 60_000 + expected_gas_limit = math.ceil(Decimal(6) * 150_000 + expected_transaction_gas_limit) assert(expected_gas_limit == transaction.fee.gas_limit) assert(str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) @@ -47,8 +46,6 @@ async def test_gas_fee_for_execute_contract_message(self): client=client, composer=composer, gas_price=5_000_000, - base_gas_limit=400_000, - base_exchange_gas_limit=150_000, ) message = composer.MsgExecuteContract( @@ -61,7 +58,8 @@ async def test_gas_fee_for_execute_contract_message(self): await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) - expected_gas_limit = math.ceil(Decimal(2.5) * 400_000) + expected_transaction_gas_limit = 60_000 + expected_gas_limit = math.ceil(Decimal(2.5) * 150_000 + expected_transaction_gas_limit) assert (expected_gas_limit == transaction.fee.gas_limit) assert (str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) @@ -74,8 +72,6 @@ async def test_gas_fee_for_wasm_message(self): client=client, composer=composer, gas_price=5_000_000, - base_gas_limit=400_000, - base_exchange_gas_limit=150_000, ) message = wasm_tx_pb2.MsgInstantiateContract2() @@ -84,7 +80,8 @@ async def test_gas_fee_for_wasm_message(self): await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) - expected_gas_limit = math.ceil(Decimal(1.5) * 400_000) + expected_transaction_gas_limit = 60_000 + expected_gas_limit = math.ceil(Decimal(1.5) * 150_000 + expected_transaction_gas_limit) assert (expected_gas_limit == transaction.fee.gas_limit) assert (str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) @@ -97,8 +94,6 @@ async def test_gas_fee_for_governance_message(self): client=client, composer=composer, gas_price=5_000_000, - base_gas_limit=400_000, - base_exchange_gas_limit=150_000, ) message = gov_tx_pb2.MsgDeposit() @@ -107,7 +102,8 @@ async def test_gas_fee_for_governance_message(self): await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) - expected_gas_limit = math.ceil(Decimal(15) * 400_000) + expected_transaction_gas_limit = 60_000 + expected_gas_limit = math.ceil(Decimal(15) * 150_000 + expected_transaction_gas_limit) assert (expected_gas_limit == transaction.fee.gas_limit) assert (str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) @@ -120,8 +116,6 @@ async def test_gas_fee_for_exchange_message(self): client=client, composer=composer, gas_price=5_000_000, - base_gas_limit=400_000, - base_exchange_gas_limit=150_000, ) message = composer.MsgCreateSpotLimitOrder( @@ -139,7 +133,8 @@ async def test_gas_fee_for_exchange_message(self): await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) - expected_gas_limit = math.ceil(Decimal(1) * 150_000) + expected_transaction_gas_limit = 60_000 + expected_gas_limit = math.ceil(Decimal(1) * 100_000 + expected_transaction_gas_limit) assert (expected_gas_limit == transaction.fee.gas_limit) assert (str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) @@ -152,8 +147,6 @@ async def test_gas_fee_for_msg_exec_message(self): client=client, composer=composer, gas_price=5_000_000, - base_gas_limit=400_000, - base_exchange_gas_limit=150_000, ) inner_message = composer.MsgCreateSpotLimitOrder( @@ -175,9 +168,12 @@ async def test_gas_fee_for_msg_exec_message(self): await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) - expected_inner_message_gas_limit = Decimal(1) * 150_000 - expected_exec_message_gas_limit = 400_000 - expected_gas_limit = math.ceil(expected_exec_message_gas_limit + expected_inner_message_gas_limit) + expected_transaction_gas_limit = 60_000 + expected_inner_message_gas_limit = Decimal(1) * 100_000 + expected_exec_message_gas_limit = 5_000 + expected_gas_limit = math.ceil( + expected_exec_message_gas_limit + expected_inner_message_gas_limit + expected_transaction_gas_limit + ) assert (expected_gas_limit == transaction.fee.gas_limit) assert (str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) @@ -190,8 +186,6 @@ async def test_gas_fee_for_two_messages_in_one_transaction(self): client=client, composer=composer, gas_price=5_000_000, - base_gas_limit=400_000, - base_exchange_gas_limit=150_000, ) inner_message = composer.MsgCreateSpotLimitOrder( @@ -221,11 +215,15 @@ async def test_gas_fee_for_two_messages_in_one_transaction(self): await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) - expected_inner_message_gas_limit = Decimal(1) * 150_000 - expected_exec_message_gas_limit = 400_000 - expected_send_message_gas_limit = 400_000 + expected_transaction_gas_limit = 60_000 + expected_inner_message_gas_limit = Decimal(1) * 100_000 + expected_exec_message_gas_limit = 5_000 + expected_send_message_gas_limit = 150_000 expected_gas_limit = math.ceil( - expected_exec_message_gas_limit + expected_inner_message_gas_limit + expected_send_message_gas_limit + expected_exec_message_gas_limit + + expected_inner_message_gas_limit + + expected_send_message_gas_limit + + expected_transaction_gas_limit ) assert (expected_gas_limit == transaction.fee.gas_limit) assert (str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) From 176e5955f134514c726b56a1b550e76e32bb71ef Mon Sep 17 00:00:00 2001 From: abel Date: Tue, 22 Aug 2023 16:07:39 -0300 Subject: [PATCH 06/17] (fix) Fix testnet URLs --- pyinjective/constant.py | 136 ++++++++++++++++++++-------------------- 1 file changed, 68 insertions(+), 68 deletions(-) diff --git a/pyinjective/constant.py b/pyinjective/constant.py index 4a263be5..57001ee3 100644 --- a/pyinjective/constant.py +++ b/pyinjective/constant.py @@ -6,13 +6,13 @@ MAX_MEMO_CHARACTERS = 256 devnet_config = ConfigParser() -devnet_config.read(os.path.join(os.path.dirname(__file__), 'denoms_devnet.ini')) +devnet_config.read(os.path.join(os.path.dirname(__file__), "denoms_devnet.ini")) testnet_config = ConfigParser() -testnet_config.read(os.path.join(os.path.dirname(__file__), 'denoms_testnet.ini')) +testnet_config.read(os.path.join(os.path.dirname(__file__), "denoms_testnet.ini")) mainnet_config = ConfigParser() -mainnet_config.read(os.path.join(os.path.dirname(__file__), 'denoms_mainnet.ini')) +mainnet_config.read(os.path.join(os.path.dirname(__file__), "denoms_mainnet.ini")) class Denom: def __init__( @@ -31,32 +31,32 @@ def __init__( @classmethod def load_market(cls, network, market_id): - if network == 'devnet': + if network == "devnet": config = devnet_config - elif network == 'testnet': + elif network == "testnet": config = testnet_config else: config =mainnet_config return cls( - description=config[market_id]['description'], - base=int(config[market_id]['base']), - quote=int(config[market_id]['quote']), - min_price_tick_size=float(config[market_id]['min_price_tick_size']), - min_quantity_tick_size=float(config[market_id]['min_quantity_tick_size']), + description=config[market_id]["description"], + base=int(config[market_id]["base"]), + quote=int(config[market_id]["quote"]), + min_price_tick_size=float(config[market_id]["min_price_tick_size"]), + min_quantity_tick_size=float(config[market_id]["min_quantity_tick_size"]), ) @classmethod def load_peggy_denom(cls, network, symbol): - if network == 'devnet': + if network == "devnet": config = devnet_config - elif network == 'local': + elif network == "local": config = devnet_config - elif network == 'testnet': + elif network == "testnet": config = testnet_config else: config = mainnet_config - return config[symbol]['peggy_denom'], int(config[symbol]['decimals']) + return config[symbol]["peggy_denom"], int(config[symbol]["decimals"]) class Network: @@ -83,14 +83,14 @@ def __init__( @classmethod def devnet(cls): return cls( - lcd_endpoint='https://devnet.lcd.injective.dev', - tm_websocket_endpoint='wss://devnet.tm.injective.dev/websocket', - grpc_endpoint='devnet.injective.dev:9900', - grpc_exchange_endpoint='devnet.injective.dev:9910', - grpc_explorer_endpoint='devnet.injective.dev:9911', - chain_id='injective-777', - fee_denom='inj', - env='devnet' + lcd_endpoint="https://devnet.lcd.injective.dev", + tm_websocket_endpoint="wss://devnet.tm.injective.dev/websocket", + grpc_endpoint="devnet.injective.dev:9900", + grpc_exchange_endpoint="devnet.injective.dev:9910", + grpc_explorer_endpoint="devnet.injective.dev:9911", + chain_id="injective-777", + fee_denom="inj", + env="devnet" ) @classmethod @@ -100,20 +100,20 @@ def testnet(cls, node="lb"): "sentry", ] if node not in nodes: - raise ValueError('Must be one of {}'.format(nodes)) - - if node == 'lb': - lcd_endpoint = 'https://k8s.testnet.lcd.injective.network', - tm_websocket_endpoint = 'wss://k8s.testnet.tm.injective.network/websocket', - grpc_endpoint = 'k8s.testnet.chain.grpc.injective.network:443', - grpc_exchange_endpoint = 'k8s.testnet.exchange.grpc.injective.network:443', - grpc_explorer_endpoint = 'k8s.testnet.explorer.grpc.injective.network:443', + raise ValueError("Must be one of {}".format(nodes)) + + if node == "lb": + lcd_endpoint = "https://k8s.testnet.lcd.injective.network" + tm_websocket_endpoint = "wss://k8s.testnet.tm.injective.network/websocket" + grpc_endpoint = "k8s.testnet.chain.grpc.injective.network:443" + grpc_exchange_endpoint = "k8s.testnet.exchange.grpc.injective.network:443" + grpc_explorer_endpoint = "k8s.testnet.explorer.grpc.injective.network:443" else: - lcd_endpoint = 'https://testnet.lcd.injective.network' - tm_websocket_endpoint = 'wss://testnet.tm.injective.network/websocket' - grpc_endpoint = 'testnet.chain.grpc.injective.network' - grpc_exchange_endpoint = 'testnet.exchange.grpc.injective.network' - grpc_explorer_endpoint = 'testnet.explorer.grpc.injective.network' + lcd_endpoint = "https://testnet.lcd.injective.network" + tm_websocket_endpoint = "wss://testnet.tm.injective.network/websocket" + grpc_endpoint = "testnet.chain.grpc.injective.network" + grpc_exchange_endpoint = "testnet.exchange.grpc.injective.network" + grpc_explorer_endpoint = "testnet.explorer.grpc.injective.network" return cls( lcd_endpoint=lcd_endpoint, @@ -121,34 +121,34 @@ def testnet(cls, node="lb"): grpc_endpoint=grpc_endpoint, grpc_exchange_endpoint=grpc_exchange_endpoint, grpc_explorer_endpoint=grpc_explorer_endpoint, - chain_id='injective-888', - fee_denom='inj', - env='testnet' + chain_id="injective-888", + fee_denom="inj", + env="testnet" ) @classmethod - def mainnet(cls, node='lb'): + def mainnet(cls, node="lb"): nodes = [ - 'lb', # us, asia, prod - 'sentry0', # ca, prod - 'sentry1', # ca, prod - 'sentry3', # us, prod + "lb", # us, asia, prod + "sentry0", # ca, prod + "sentry1", # ca, prod + "sentry3", # us, prod ] if node not in nodes: - raise ValueError('Must be one of {}'.format(nodes)) - - if node == 'lb': - lcd_endpoint = 'https://k8s.global.mainnet.lcd.injective.network:443' - tm_websocket_endpoint = 'wss://k8s.global.mainnet.tm.injective.network:443/websocket' - grpc_endpoint = 'k8s.global.mainnet.chain.grpc.injective.network:443' - grpc_exchange_endpoint = 'k8s.global.mainnet.exchange.grpc.injective.network:443' - grpc_explorer_endpoint = 'k8s.global.mainnet.explorer.grpc.injective.network:443' + raise ValueError("Must be one of {}".format(nodes)) + + if node == "lb": + lcd_endpoint = "https://k8s.global.mainnet.lcd.injective.network:443" + tm_websocket_endpoint = "wss://k8s.global.mainnet.tm.injective.network:443/websocket" + grpc_endpoint = "k8s.global.mainnet.chain.grpc.injective.network:443" + grpc_exchange_endpoint = "k8s.global.mainnet.exchange.grpc.injective.network:443" + grpc_explorer_endpoint = "k8s.global.mainnet.explorer.grpc.injective.network:443" else: - lcd_endpoint=f'http://{node}.injective.network:10337' - tm_websocket_endpoint=f'ws://{node}.injective.network:26657/websocket' - grpc_endpoint=f'{node}.injective.network:9900' - grpc_exchange_endpoint=f'{node}.injective.network:9910' - grpc_explorer_endpoint=f'{node}.injective.network:9911' + lcd_endpoint = f"http://{node}.injective.network:10337" + tm_websocket_endpoint = f"ws://{node}.injective.network:26657/websocket" + grpc_endpoint = f"{node}.injective.network:9900" + grpc_exchange_endpoint = f"{node}.injective.network:9910" + grpc_explorer_endpoint = f"{node}.injective.network:9911" return cls( lcd_endpoint=lcd_endpoint, @@ -156,22 +156,22 @@ def mainnet(cls, node='lb'): grpc_endpoint=grpc_endpoint, grpc_exchange_endpoint=grpc_exchange_endpoint, grpc_explorer_endpoint=grpc_explorer_endpoint, - chain_id='injective-1', - fee_denom='inj', - env='mainnet' + chain_id="injective-1", + fee_denom="inj", + env="mainnet" ) @classmethod def local(cls): return cls( - lcd_endpoint='http://localhost:10337', - tm_websocket_endpoint='ws://localhost:26657/websocket', - grpc_endpoint='localhost:9900', - grpc_exchange_endpoint='localhost:9910', - grpc_explorer_endpoint='localhost:9911', - chain_id='injective-1', - fee_denom='inj', - env='local' + lcd_endpoint="http://localhost:10337", + tm_websocket_endpoint="ws://localhost:26657/websocket", + grpc_endpoint="localhost:9900", + grpc_exchange_endpoint="localhost:9910", + grpc_explorer_endpoint="localhost:9911", + chain_id="injective-1", + fee_denom="inj", + env="local" ) @classmethod @@ -183,7 +183,7 @@ def custom(cls, lcd_endpoint, tm_websocket_endpoint, grpc_endpoint, grpc_exchang grpc_exchange_endpoint=grpc_exchange_endpoint, grpc_explorer_endpoint=grpc_explorer_endpoint, chain_id=chain_id, - fee_denom='inj', + fee_denom="inj", env=env ) From af5c62cb83cb7aa7f17e033e8fdf2a4c48042248 Mon Sep 17 00:00:00 2001 From: abel Date: Tue, 22 Aug 2023 16:15:45 -0300 Subject: [PATCH 07/17] (fix) Updated library version number and README file --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index d5b19728..026b2995 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,9 @@ make tests **0.7.2** * Added a new gas limit calculation for the TransactionBroadcaster that estimates the value based on the messages in the transaction (without running the transaction simulation). +**0.7.1.1** +* Fixed Testnet network URLs + **0.7.1** * Include implementation of the TransactionBroadcaster, to simplify the transaction creation and broadcasting process. From 2792998faadd7bd8c6500b877c8471f8ae4afeda Mon Sep 17 00:00:00 2001 From: Morgan Metz <68765538+Morgandri1@users.noreply.github.com> Date: Thu, 24 Aug 2023 11:24:16 -0400 Subject: [PATCH 08/17] Update denoms_mainnet.ini Add NBLA denom to peggy --- pyinjective/denoms_mainnet.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyinjective/denoms_mainnet.ini b/pyinjective/denoms_mainnet.ini index 691a4b34..b6e0080f 100644 --- a/pyinjective/denoms_mainnet.ini +++ b/pyinjective/denoms_mainnet.ini @@ -686,3 +686,6 @@ decimals = 8 peggy_denom = factory/inj14ejqjyq8um4p3xfqj74yld5waqljf88f9eneuk/inj1dxv423h8ygzgxmxnvrf33ws3k94aedfdevxd8h decimals = 8 +[NBLA] +peggy_denom = factory/inj1d0zfq42409a5mhdagjutl8u6u9rgcm4h8zfmfq/nbla +decimals = 6 From 2b874329a0b8d623d4b06fa9eb46fd129e10beaa Mon Sep 17 00:00:00 2001 From: Achilleas Kalantzis Date: Thu, 24 Aug 2023 18:30:50 +0300 Subject: [PATCH 09/17] chore: add NBLA --- README.md | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b77cf52a..3845f5e0 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,9 @@ make tests ``` ### Changelogs +**0.7.1.2** +* Add NBLA + **0.7.1.1** * Fixed Testnet network URLs diff --git a/setup.py b/setup.py index 4060de98..155e7503 100755 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ EMAIL = "achilleas@injectivelabs.com" AUTHOR = "Injective Labs" REQUIRES_PYTHON = ">=3.9" -VERSION = "0.7.1.1" +VERSION = "0.7.1.2" REQUIRED = [ "aiohttp", From ccabe013066f474dc679e44ef26375140bd81f1e Mon Sep 17 00:00:00 2001 From: Morgan Metz <68765538+Morgandri1@users.noreply.github.com> Date: Thu, 24 Aug 2023 11:24:16 -0400 Subject: [PATCH 10/17] Update denoms_mainnet.ini Add NBLA denom to peggy --- pyinjective/denoms_mainnet.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyinjective/denoms_mainnet.ini b/pyinjective/denoms_mainnet.ini index 691a4b34..b6e0080f 100644 --- a/pyinjective/denoms_mainnet.ini +++ b/pyinjective/denoms_mainnet.ini @@ -686,3 +686,6 @@ decimals = 8 peggy_denom = factory/inj14ejqjyq8um4p3xfqj74yld5waqljf88f9eneuk/inj1dxv423h8ygzgxmxnvrf33ws3k94aedfdevxd8h decimals = 8 +[NBLA] +peggy_denom = factory/inj1d0zfq42409a5mhdagjutl8u6u9rgcm4h8zfmfq/nbla +decimals = 6 From d71b813274fe0d4d8089d10f82316a1e7b01a424 Mon Sep 17 00:00:00 2001 From: Achilleas Kalantzis Date: Thu, 24 Aug 2023 18:30:50 +0300 Subject: [PATCH 11/17] chore: add NBLA --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 026b2995..67fa1bd4 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,9 @@ make tests **0.7.2** * Added a new gas limit calculation for the TransactionBroadcaster that estimates the value based on the messages in the transaction (without running the transaction simulation). +**0.7.1.2** +* Add NBLA + **0.7.1.1** * Fixed Testnet network URLs From cd3fa2775215f17fda4033115efaa37c409f2c26 Mon Sep 17 00:00:00 2001 From: abel Date: Fri, 18 Aug 2023 00:55:26 -0300 Subject: [PATCH 12/17] (feat) Added a component to calculate gas limit for the Transaction Broadcaster based on the messages (without running the transaction simulation) --- .../46_MessageBroadcasterWithoutSimulation.py | 64 +++++ ...sterWithGranteeAccountWithoutSimulation.py | 54 ++++ .../explorer_rpc/1_GetTxByHash.py | 4 +- pyinjective/core/broadcaster.py | 155 +++++++++++- ...essage_based_transaction_fee_calculator.py | 231 ++++++++++++++++++ 5 files changed, 503 insertions(+), 5 deletions(-) create mode 100644 examples/chain_client/46_MessageBroadcasterWithoutSimulation.py create mode 100644 examples/chain_client/47_MessageBroadcasterWithGranteeAccountWithoutSimulation.py create mode 100644 tests/core/test_message_based_transaction_fee_calculator.py diff --git a/examples/chain_client/46_MessageBroadcasterWithoutSimulation.py b/examples/chain_client/46_MessageBroadcasterWithoutSimulation.py new file mode 100644 index 00000000..847c79a9 --- /dev/null +++ b/examples/chain_client/46_MessageBroadcasterWithoutSimulation.py @@ -0,0 +1,64 @@ +import asyncio + +from pyinjective.composer import Composer as ProtoMsgComposer +from pyinjective.core.broadcaster import MsgBroadcasterWithPk +from pyinjective.constant import Network +from pyinjective.wallet import PrivateKey + + +async def main() -> None: + # select network: local, testnet, mainnet + network = Network.testnet() + composer = ProtoMsgComposer(network=network.string()) + private_key_in_hexa = "f9db9bf330e23cb7839039e944adef6e9df447b90b503d5b4464c90bea9022f3" + + message_broadcaster = MsgBroadcasterWithPk.new_without_simulation( + network=network, + private_key=private_key_in_hexa, + use_secure_connection=True + ) + + priv_key = PrivateKey.from_hex(private_key_in_hexa) + pub_key = priv_key.to_public_key() + address = pub_key.to_address() + subaccount_id = address.get_subaccount_id(index=0) + + # prepare trade info + fee_recipient = "inj1hkhdaj2a2clmq5jq6mspsggqs32vynpk228q3r" + + spot_market_id_create = "0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe" + + spot_orders_to_create = [ + composer.SpotOrder( + market_id=spot_market_id_create, + subaccount_id=subaccount_id, + fee_recipient=fee_recipient, + price=3, + quantity=55, + is_buy=True, + is_po=False + ), + composer.SpotOrder( + market_id=spot_market_id_create, + subaccount_id=subaccount_id, + fee_recipient=fee_recipient, + price=300, + quantity=55, + is_buy=False, + is_po=False + ), + ] + + # prepare tx msg + msg = composer.MsgBatchUpdateOrders( + sender=address.to_acc_bech32(), + spot_orders_to_create=spot_orders_to_create, + ) + + # broadcast the transaction + result = await message_broadcaster.broadcast([msg]) + print("---Transaction Response---") + print(result) + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/47_MessageBroadcasterWithGranteeAccountWithoutSimulation.py b/examples/chain_client/47_MessageBroadcasterWithGranteeAccountWithoutSimulation.py new file mode 100644 index 00000000..b3311279 --- /dev/null +++ b/examples/chain_client/47_MessageBroadcasterWithGranteeAccountWithoutSimulation.py @@ -0,0 +1,54 @@ +import asyncio + +from pyinjective.composer import Composer as ProtoMsgComposer +from pyinjective.async_client import AsyncClient +from pyinjective.core.broadcaster import MsgBroadcasterWithPk +from pyinjective.constant import Network +from pyinjective.wallet import PrivateKey, Address + + +async def main() -> None: + # select network: local, testnet, mainnet + network = Network.testnet() + composer = ProtoMsgComposer(network=network.string()) + + # initialize grpc client + client = AsyncClient(network, insecure=False) + await client.sync_timeout_height() + + # load account + private_key_in_hexa = "5d386fbdbf11f1141010f81a46b40f94887367562bd33b452bbaa6ce1cd1381e" + priv_key = PrivateKey.from_hex(private_key_in_hexa) + pub_key = priv_key.to_public_key() + address = pub_key.to_address() + + message_broadcaster = MsgBroadcasterWithPk.new_for_grantee_account_without_simulation( + network=network, + grantee_private_key=private_key_in_hexa, + use_secure_connection=True + ) + + # prepare tx msg + market_id = "0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe" + granter_inj_address = "inj1hkhdaj2a2clmq5jq6mspsggqs32vynpk228q3r" + granter_address = Address.from_acc_bech32(granter_inj_address) + granter_subaccount_id = granter_address.get_subaccount_id(index=0) + + msg = composer.MsgCreateSpotLimitOrder( + sender=granter_inj_address, + market_id=market_id, + subaccount_id=granter_subaccount_id, + fee_recipient=address.to_acc_bech32(), + price=7.523, + quantity=0.01, + is_buy=True, + is_po=False + ) + + # broadcast the transaction + result = await message_broadcaster.broadcast([msg]) + print("---Transaction Response---") + print(result) + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/exchange_client/explorer_rpc/1_GetTxByHash.py b/examples/exchange_client/explorer_rpc/1_GetTxByHash.py index 7362d1e0..fa21e293 100644 --- a/examples/exchange_client/explorer_rpc/1_GetTxByHash.py +++ b/examples/exchange_client/explorer_rpc/1_GetTxByHash.py @@ -7,10 +7,10 @@ async def main() -> None: # select network: local, testnet, mainnet - network = Network.testnet() + network = Network.mainnet() client = AsyncClient(network, insecure=False) composer = Composer(network=network.string()) - tx_hash = "0F3EBEC1882E1EEAC5B7BDD836E976250F1CD072B79485877CEACCB92ACDDF52" + tx_hash = "50DD80270052D85835939A393127B5626917007E71A7ABD5205F1A094B976C1B" transaction_response = await client.get_tx_by_hash(tx_hash=tx_hash) print(transaction_response) diff --git a/pyinjective/core/broadcaster.py b/pyinjective/core/broadcaster.py index d213d8de..fb9f7eed 100644 --- a/pyinjective/core/broadcaster.py +++ b/pyinjective/core/broadcaster.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod from decimal import Decimal -from typing import List, Optional +from typing import Callable, List, Optional, Tuple import math from google.protobuf import any_pb2 @@ -9,6 +9,7 @@ from pyinjective.async_client import AsyncClient from pyinjective.composer import Composer from pyinjective.constant import Network +from pyinjective.proto.cosmos.authz.v1beta1 import tx_pb2 as cosmos_authz_tx_pb class BroadcasterAccountConfig(ABC): @@ -86,6 +87,31 @@ def new_using_simulation( ) return instance + @classmethod + def new_without_simulation( + cls, + network: Network, + private_key: str, + use_secure_connection: bool = True, + client: Optional[AsyncClient] = None, + composer: Optional[Composer] = None, + ): + client = client or AsyncClient(network=network, insecure=not (use_secure_connection)) + composer = composer or Composer(network=client.network.string()) + account_config = StandardAccountBroadcasterConfig(private_key=private_key) + fee_calculator = MessageBasedTransactionFeeCalculator( + client=client, + composer=composer + ) + instance = cls( + network=network, + account_config=account_config, + client=client, + composer=composer, + fee_calculator=fee_calculator, + ) + return instance + @classmethod def new_for_grantee_account_using_simulation( cls, @@ -111,6 +137,31 @@ def new_for_grantee_account_using_simulation( ) return instance + @classmethod + def new_for_grantee_account_without_simulation( + cls, + network: Network, + grantee_private_key: str, + use_secure_connection: bool = True, + client: Optional[AsyncClient] = None, + composer: Optional[Composer] = None, + ): + client = client or AsyncClient(network=network, insecure=not (use_secure_connection)) + composer = composer or Composer(network=client.network.string()) + account_config = GranteeAccountBroadcasterConfig(grantee_private_key=grantee_private_key, composer=composer) + fee_calculator = MessageBasedTransactionFeeCalculator( + client=client, + composer=composer + ) + instance = cls( + network=network, + account_config=account_config, + client=client, + composer=composer, + fee_calculator=fee_calculator, + ) + return instance + async def broadcast(self, messages: List[any_pb2.Any]): await self._client.sync_timeout_height() await self._client.get_account(self._account_config.trading_injective_address) @@ -196,10 +247,16 @@ def messages_prepared_for_transaction(self, messages: List[any_pb2.Any]) -> List class SimulatedTransactionFeeCalculator(TransactionFeeCalculator): - def __init__(self, client: AsyncClient, composer: Composer, gas_price: Optional[int] = None): + def __init__( + self, + client: AsyncClient, + composer: Composer, + gas_price: Optional[int] = None, + gas_limit_adjustment_multiplier: Optional[Decimal] = None): self._client = client self._composer = composer self._gas_price = gas_price or self.DEFAULT_GAS_PRICE + self._gas_limit_adjustment_multiplier = gas_limit_adjustment_multiplier or Decimal("1.3") async def configure_gas_fee_for_transaction( self, @@ -216,7 +273,7 @@ async def configure_gas_fee_for_transaction( if not success: raise RuntimeError(f"Transaction simulation error: {sim_res}") - gas_limit = math.ceil(Decimal(str(sim_res.gas_info.gas_used)) * Decimal("1.1")) + gas_limit = math.ceil(Decimal(str(sim_res.gas_info.gas_used)) * self._gas_limit_adjustment_multiplier) fee = [ self._composer.Coin( @@ -227,3 +284,95 @@ async def configure_gas_fee_for_transaction( transaction.with_gas(gas=gas_limit) transaction.with_fee(fee=fee) + + +class MessageBasedTransactionFeeCalculator(TransactionFeeCalculator): + DEFAULT_GAS_LIMIT = 400_000 + DEFAULT_EXCHANGE_GAS_LIMIT = 200_000 + + def __init__( + self, + client: AsyncClient, + composer: Composer, + gas_price: Optional[int] = None, + base_gas_limit: Optional[int] = None, + base_exchange_gas_limit: Optional[int] = None): + self._client = client + self._composer = composer + self._gas_price = gas_price or self.DEFAULT_GAS_PRICE + self._base_gas_limit = base_gas_limit or self.DEFAULT_GAS_LIMIT + self._base_exchange_gas_limit = base_exchange_gas_limit or self.DEFAULT_EXCHANGE_GAS_LIMIT + + self._gas_limit_per_message_type_rules: List[Tuple[Callable, Decimal]] = [ + ( + lambda message: "MsgPrivilegedExecuteContract" in self._message_type(message=message), + Decimal("6") * self._base_gas_limit + ), + ( + lambda message: "MsgExecuteContract" in self._message_type(message=message), + Decimal("2.5") * self._base_gas_limit + ), + ( + lambda message: "wasm" in self._message_type(message=message), + Decimal("1.5") * self._base_gas_limit + ), + ( + lambda message: "exchange" in self._message_type(message=message), + Decimal("1") * self._base_exchange_gas_limit + ), + ( + lambda message: self._is_governance_message(message=message), + Decimal("15") * self._base_gas_limit + ), + ] + + async def configure_gas_fee_for_transaction( + self, + transaction: Transaction, + private_key: PrivateKey, + public_key: PublicKey, + ): + transaction_gas_limit = math.ceil(self._calculate_gas_limit(messages=transaction.msgs)) + + fee = [ + self._composer.Coin( + amount=math.ceil(self._gas_price * transaction_gas_limit), + denom=self._client.network.fee_denom, + ) + ] + + transaction.with_gas(gas=transaction_gas_limit) + transaction.with_fee(fee=fee) + + def _message_type(self, message: any_pb2.Any) -> str: + if isinstance(message, any_pb2.Any): + message_type = message.type_url + else: + message_type = message.DESCRIPTOR.full_name + return message_type + + def _is_governance_message(self, message: any_pb2.Any) -> bool: + message_type = self._message_type(message=message) + return "gov" in message_type and ("MsgDeposit" in message_type or "MsgSubmitProposal" in message_type) + + def _calculate_gas_limit(self, messages: List[any_pb2.Any]) -> int: + total_gas_limit = Decimal("0") + + for message in messages: + applying_rule = next( + (rule_tuple for rule_tuple in self._gas_limit_per_message_type_rules + if rule_tuple[0](message)), + None + ) + + if applying_rule is None: + total_gas_limit += self._base_gas_limit + else: + total_gas_limit += applying_rule[1] + + if self._message_type(message=message).endswith("MsgExec"): + exec_message = cosmos_authz_tx_pb.MsgExec.FromString(message.value) + sub_messages_limit = self._calculate_gas_limit(messages=exec_message.msgs) + total_gas_limit += sub_messages_limit + + return math.ceil(total_gas_limit) diff --git a/tests/core/test_message_based_transaction_fee_calculator.py b/tests/core/test_message_based_transaction_fee_calculator.py new file mode 100644 index 00000000..14bfea81 --- /dev/null +++ b/tests/core/test_message_based_transaction_fee_calculator.py @@ -0,0 +1,231 @@ +from decimal import Decimal + +import math +import pytest + +from pyinjective import Transaction +from pyinjective.async_client import AsyncClient +from pyinjective.composer import Composer +from pyinjective.constant import Network +from pyinjective.core.broadcaster import MessageBasedTransactionFeeCalculator +from pyinjective.proto.cosmos.gov.v1beta1 import tx_pb2 as gov_tx_pb2 +from pyinjective.proto.cosmwasm.wasm.v1 import tx_pb2 as wasm_tx_pb2 +from pyinjective.proto.injective.exchange.v1beta1 import tx_pb2 + + +class TestMessageBasedTransactionFeeCalculator: + + @pytest.mark.asyncio + async def test_gas_fee_for_privileged_execute_contract_message(self): + network = Network.testnet(node="sentry") + client = AsyncClient(network=network, insecure=False) + composer = Composer(network=network.string()) + calculator = MessageBasedTransactionFeeCalculator( + client=client, + composer=composer, + gas_price=5_000_000, + base_gas_limit=400_000, + base_exchange_gas_limit=150_000, + ) + + message = tx_pb2.MsgPrivilegedExecuteContract() + transaction = Transaction() + transaction.with_messages(message) + + await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) + + expected_gas_limit = math.ceil(Decimal(6) * 400_000) + assert(expected_gas_limit == transaction.fee.gas_limit) + assert(str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) + + @pytest.mark.asyncio + async def test_gas_fee_for_execute_contract_message(self): + network = Network.testnet(node="sentry") + client = AsyncClient(network=network, insecure=False) + composer = Composer(network=network.string()) + calculator = MessageBasedTransactionFeeCalculator( + client=client, + composer=composer, + gas_price=5_000_000, + base_gas_limit=400_000, + base_exchange_gas_limit=150_000, + ) + + message = composer.MsgExecuteContract( + sender="", + contract="", + msg="", + ) + transaction = Transaction() + transaction.with_messages(message) + + await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) + + expected_gas_limit = math.ceil(Decimal(2.5) * 400_000) + assert (expected_gas_limit == transaction.fee.gas_limit) + assert (str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) + + @pytest.mark.asyncio + async def test_gas_fee_for_wasm_message(self): + network = Network.testnet(node="sentry") + client = AsyncClient(network=network, insecure=False) + composer = Composer(network=network.string()) + calculator = MessageBasedTransactionFeeCalculator( + client=client, + composer=composer, + gas_price=5_000_000, + base_gas_limit=400_000, + base_exchange_gas_limit=150_000, + ) + + message = wasm_tx_pb2.MsgInstantiateContract2() + transaction = Transaction() + transaction.with_messages(message) + + await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) + + expected_gas_limit = math.ceil(Decimal(1.5) * 400_000) + assert (expected_gas_limit == transaction.fee.gas_limit) + assert (str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) + + @pytest.mark.asyncio + async def test_gas_fee_for_governance_message(self): + network = Network.testnet(node="sentry") + client = AsyncClient(network=network, insecure=False) + composer = Composer(network=network.string()) + calculator = MessageBasedTransactionFeeCalculator( + client=client, + composer=composer, + gas_price=5_000_000, + base_gas_limit=400_000, + base_exchange_gas_limit=150_000, + ) + + message = gov_tx_pb2.MsgDeposit() + transaction = Transaction() + transaction.with_messages(message) + + await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) + + expected_gas_limit = math.ceil(Decimal(15) * 400_000) + assert (expected_gas_limit == transaction.fee.gas_limit) + assert (str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) + + @pytest.mark.asyncio + async def test_gas_fee_for_exchange_message(self): + network = Network.testnet(node="sentry") + client = AsyncClient(network=network, insecure=False) + composer = Composer(network=network.string()) + calculator = MessageBasedTransactionFeeCalculator( + client=client, + composer=composer, + gas_price=5_000_000, + base_gas_limit=400_000, + base_exchange_gas_limit=150_000, + ) + + message = composer.MsgCreateSpotLimitOrder( + sender="sender", + market_id="0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe", + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=7.523, + quantity=0.01, + is_buy=True, + is_po=False + ) + transaction = Transaction() + transaction.with_messages(message) + + await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) + + expected_gas_limit = math.ceil(Decimal(1) * 150_000) + assert (expected_gas_limit == transaction.fee.gas_limit) + assert (str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) + + @pytest.mark.asyncio + async def test_gas_fee_for_msg_exec_message(self): + network = Network.testnet(node="sentry") + client = AsyncClient(network=network, insecure=False) + composer = Composer(network=network.string()) + calculator = MessageBasedTransactionFeeCalculator( + client=client, + composer=composer, + gas_price=5_000_000, + base_gas_limit=400_000, + base_exchange_gas_limit=150_000, + ) + + inner_message = composer.MsgCreateSpotLimitOrder( + sender="sender", + market_id="0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe", + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=7.523, + quantity=0.01, + is_buy=True, + is_po=False + ) + message = composer.MsgExec( + grantee="grantee", + msgs=[inner_message] + ) + transaction = Transaction() + transaction.with_messages(message) + + await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) + + expected_inner_message_gas_limit = Decimal(1) * 150_000 + expected_exec_message_gas_limit = 400_000 + expected_gas_limit = math.ceil(expected_exec_message_gas_limit + expected_inner_message_gas_limit) + assert (expected_gas_limit == transaction.fee.gas_limit) + assert (str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) + + @pytest.mark.asyncio + async def test_gas_fee_for_two_messages_in_one_transaction(self): + network = Network.testnet(node="sentry") + client = AsyncClient(network=network, insecure=False) + composer = Composer(network=network.string()) + calculator = MessageBasedTransactionFeeCalculator( + client=client, + composer=composer, + gas_price=5_000_000, + base_gas_limit=400_000, + base_exchange_gas_limit=150_000, + ) + + inner_message = composer.MsgCreateSpotLimitOrder( + sender="sender", + market_id="0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe", + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=7.523, + quantity=0.01, + is_buy=True, + is_po=False + ) + message = composer.MsgExec( + grantee="grantee", + msgs=[inner_message] + ) + + send_message = composer.MsgSend( + from_address="address", + to_address='to_address', + amount=1, + denom='INJ' + ) + + transaction = Transaction() + transaction.with_messages(message, send_message) + + await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) + + expected_inner_message_gas_limit = Decimal(1) * 150_000 + expected_exec_message_gas_limit = 400_000 + expected_send_message_gas_limit = 400_000 + expected_gas_limit = math.ceil( + expected_exec_message_gas_limit + expected_inner_message_gas_limit + expected_send_message_gas_limit + ) + assert (expected_gas_limit == transaction.fee.gas_limit) + assert (str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) From 5cd4e061efdfb5ab4e64cb971e0b951f8fdc6419 Mon Sep 17 00:00:00 2001 From: abel Date: Fri, 18 Aug 2023 00:58:52 -0300 Subject: [PATCH 13/17] (fix) Change version number and update README file --- README.md | 5 ++++- setup.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3845f5e0..67fa1bd4 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,9 @@ make tests ``` ### Changelogs +**0.7.2** +* Added a new gas limit calculation for the TransactionBroadcaster that estimates the value based on the messages in the transaction (without running the transaction simulation). + **0.7.1.2** * Add NBLA @@ -94,7 +97,7 @@ make tests * Fixed Testnet network URLs **0.7.1** -* Include implementation of the MessageBroadcaster, to simplify the transaction creation and broadcasting process. +* Include implementation of the TransactionBroadcaster, to simplify the transaction creation and broadcasting process. **0.7.0.6** * ADD SEI/USDT in metadata diff --git a/setup.py b/setup.py index 155e7503..8481af42 100755 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ EMAIL = "achilleas@injectivelabs.com" AUTHOR = "Injective Labs" REQUIRES_PYTHON = ">=3.9" -VERSION = "0.7.1.2" +VERSION = "0.7.2" REQUIRED = [ "aiohttp", From aeeb1d368b0438c435404e16767274c15a23b2cc Mon Sep 17 00:00:00 2001 From: abel Date: Fri, 18 Aug 2023 12:47:44 -0300 Subject: [PATCH 14/17] (fix) Changes to gas limit estimation to reduce a little bit gas consumption --- pyinjective/async_client.py | 44 ++++++++++++++------------------- pyinjective/core/broadcaster.py | 10 ++++---- 2 files changed, 24 insertions(+), 30 deletions(-) diff --git a/pyinjective/async_client.py b/pyinjective/async_client.py index 96e88db4..965d1549 100644 --- a/pyinjective/async_client.py +++ b/pyinjective/async_client.py @@ -302,7 +302,7 @@ async def get_account(self, address: str) -> Optional[account_pb2.EthAccount]: try: metadata = await self.load_cookie(type="chain") account_any = (await self.stubAuth.Account( - auth_query.QueryAccountRequest.__call__(address=address), metadata=metadata + auth_query.QueryAccountRequest(address=address), metadata=metadata )).account account = account_pb2.EthAccount() if account_any.Is(account.DESCRIPTOR): @@ -339,7 +339,7 @@ async def simulate_tx( try: req = tx_service.SimulateRequest(tx_bytes=tx_byte) metadata = await self.load_cookie(type="chain") - return await self.stubTx.Simulate.__call__(req, metadata=metadata), True + return await self.stubTx.Simulate(request=req, metadata=metadata), True except grpc.RpcError as err: return err, False @@ -348,7 +348,7 @@ async def send_tx_sync_mode(self, tx_byte: bytes) -> abci_type.TxResponse: tx_bytes=tx_byte, mode=tx_service.BroadcastMode.BROADCAST_MODE_SYNC ) metadata = await self.load_cookie(type="chain") - result = await self.stubTx.BroadcastTx.__call__(req, metadata=metadata) + result = await self.stubTx.BroadcastTx(request=req, metadata=metadata) return result.tx_response async def send_tx_async_mode(self, tx_byte: bytes) -> abci_type.TxResponse: @@ -356,7 +356,7 @@ async def send_tx_async_mode(self, tx_byte: bytes) -> abci_type.TxResponse: tx_bytes=tx_byte, mode=tx_service.BroadcastMode.BROADCAST_MODE_ASYNC ) metadata = await self.load_cookie(type="chain") - result = await self.stubTx.BroadcastTx.__call__(req, metadata=metadata) + result = await self.stubTx.BroadcastTx(request=req, metadata=metadata) return result.tx_response async def send_tx_block_mode(self, tx_byte: bytes) -> abci_type.TxResponse: @@ -364,7 +364,7 @@ async def send_tx_block_mode(self, tx_byte: bytes) -> abci_type.TxResponse: tx_bytes=tx_byte, mode=tx_service.BroadcastMode.BROADCAST_MODE_BLOCK ) metadata = await self.load_cookie(type="chain") - result = await self.stubTx.BroadcastTx.__call__(req, metadata=metadata) + result = await self.stubTx.BroadcastTx(request=req, metadata=metadata) return result.tx_response async def get_chain_id(self) -> str: @@ -632,7 +632,7 @@ async def stream_spot_markets(self, **kwargs): market_ids=kwargs.get("market_ids") ) metadata = await self.load_cookie(type="exchange") - return self.stubSpotExchange.StreamMarkets.__call__(req, metadata=metadata) + return self.stubSpotExchange.StreamMarkets(request=req, metadata=metadata) async def get_spot_orderbookV2(self, market_id: str): req = spot_exchange_rpc_pb.OrderbookV2Request(market_id=market_id) @@ -689,12 +689,12 @@ async def get_spot_trades(self, **kwargs): async def stream_spot_orderbook_snapshot(self, market_ids: List[str]): req = spot_exchange_rpc_pb.StreamOrderbookV2Request(market_ids=market_ids) metadata = await self.load_cookie(type="exchange") - return self.stubSpotExchange.StreamOrderbookV2.__call__(req, metadata=metadata) + return self.stubSpotExchange.StreamOrderbookV2(request=req, metadata=metadata) async def stream_spot_orderbook_update(self, market_ids: List[str]): req = spot_exchange_rpc_pb.StreamOrderbookUpdateRequest(market_ids=market_ids) metadata = await self.load_cookie(type="exchange") - return self.stubSpotExchange.StreamOrderbookUpdate.__call__(req, metadata=metadata) + return self.stubSpotExchange.StreamOrderbookUpdate(request=req, metadata=metadata) async def stream_spot_orders(self, market_id: str, **kwargs): req = spot_exchange_rpc_pb.StreamOrdersRequest( @@ -703,7 +703,7 @@ async def stream_spot_orders(self, market_id: str, **kwargs): subaccount_id=kwargs.get("subaccount_id"), ) metadata = await self.load_cookie(type="exchange") - return self.stubSpotExchange.StreamOrders.__call__(req, metadata=metadata) + return self.stubSpotExchange.StreamOrders(request=req, metadata=metadata) async def stream_historical_spot_orders(self, market_id: str, **kwargs): req = spot_exchange_rpc_pb.StreamOrdersHistoryRequest( @@ -715,7 +715,7 @@ async def stream_historical_spot_orders(self, market_id: str, **kwargs): execution_types=kwargs.get("execution_types") ) metadata = await self.load_cookie(type="exchange") - return self.stubSpotExchange.StreamOrdersHistory.__call__(req, metadata=metadata) + return self.stubSpotExchange.StreamOrdersHistory(request=req, metadata=metadata) async def stream_historical_derivative_orders(self, market_id: str, **kwargs): req = derivative_exchange_rpc_pb.StreamOrdersHistoryRequest( @@ -727,7 +727,7 @@ async def stream_historical_derivative_orders(self, market_id: str, **kwargs): execution_types=kwargs.get("execution_types") ) metadata = await self.load_cookie(type="exchange") - return self.stubDerivativeExchange.StreamOrdersHistory.__call__(req, metadata=metadata) + return self.stubDerivativeExchange.StreamOrdersHistory(request=req, metadata=metadata) async def stream_spot_trades(self, **kwargs): req = spot_exchange_rpc_pb.StreamTradesRequest( @@ -740,7 +740,7 @@ async def stream_spot_trades(self, **kwargs): execution_types=kwargs.get("execution_types"), ) metadata = await self.load_cookie(type="exchange") - return self.stubSpotExchange.StreamTrades.__call__(req, metadata=metadata) + return self.stubSpotExchange.StreamTrades(request=req, metadata=metadata) async def get_spot_subaccount_orders(self, subaccount_id: str, **kwargs): req = spot_exchange_rpc_pb.SubaccountOrdersListRequest( @@ -780,7 +780,7 @@ async def stream_derivative_markets(self, **kwargs): market_ids=kwargs.get("market_ids") ) metadata = await self.load_cookie(type="exchange") - return self.stubDerivativeExchange.StreamMarket.__call__(req, metadata=metadata) + return self.stubDerivativeExchange.StreamMarket(request=req, metadata=metadata) async def get_derivative_orderbook(self, market_id: str): req = derivative_exchange_rpc_pb.OrderbookV2Request(market_id=market_id) @@ -846,16 +846,12 @@ async def get_derivative_trades(self, **kwargs): async def stream_derivative_orderbook_snapshot(self, market_ids: List[str]): req = derivative_exchange_rpc_pb.StreamOrderbookV2Request(market_ids=market_ids) metadata = await self.load_cookie(type="exchange") - return self.stubDerivativeExchange.StreamOrderbookV2.__call__( - req, metadata=metadata - ) + return self.stubDerivativeExchange.StreamOrderbookV2(request=req, metadata=metadata) async def stream_derivative_orderbook_update(self, market_ids: List[str]): req = derivative_exchange_rpc_pb.StreamOrderbookUpdateRequest(market_ids=market_ids) metadata = await self.load_cookie(type="exchange") - return self.stubDerivativeExchange.StreamOrderbookUpdate.__call__( - req, metadata=metadata - ) + return self.stubDerivativeExchange.StreamOrderbookUpdate(request=req, metadata=metadata) async def stream_derivative_orders(self, market_id: str, **kwargs): req = derivative_exchange_rpc_pb.StreamOrdersRequest( @@ -864,7 +860,7 @@ async def stream_derivative_orders(self, market_id: str, **kwargs): subaccount_id=kwargs.get("subaccount_id"), ) metadata = await self.load_cookie(type="exchange") - return self.stubDerivativeExchange.StreamOrders.__call__(req, metadata=metadata) + return self.stubDerivativeExchange.StreamOrders(request=req, metadata=metadata) async def stream_derivative_trades(self, **kwargs): req = derivative_exchange_rpc_pb.StreamTradesRequest( @@ -879,7 +875,7 @@ async def stream_derivative_trades(self, **kwargs): execution_types=kwargs.get("execution_types"), ) metadata = await self.load_cookie(type="exchange") - return self.stubDerivativeExchange.StreamTrades.__call__(req, metadata=metadata) + return self.stubDerivativeExchange.StreamTrades(request=req, metadata=metadata) async def get_derivative_positions(self, **kwargs): req = derivative_exchange_rpc_pb.PositionsRequest( @@ -901,9 +897,7 @@ async def stream_derivative_positions(self, **kwargs): subaccount_ids=kwargs.get("subaccount_ids"), ) metadata = await self.load_cookie(type="exchange") - return self.stubDerivativeExchange.StreamPositions.__call__( - req, metadata=metadata - ) + return self.stubDerivativeExchange.StreamPositions(request=req, metadata=metadata) async def get_derivative_liquidable_positions(self, **kwargs): req = derivative_exchange_rpc_pb.LiquidablePositionsRequest( @@ -979,4 +973,4 @@ async def stream_account_portfolio(self, account_address: str, **kwargs): type=kwargs.get("type") ) metadata = await self.load_cookie(type="exchange") - return self.stubPortfolio.StreamAccountPortfolio.__call__(req, metadata=metadata) + return self.stubPortfolio.StreamAccountPortfolio(request=req, metadata=metadata) diff --git a/pyinjective/core/broadcaster.py b/pyinjective/core/broadcaster.py index fb9f7eed..55606b85 100644 --- a/pyinjective/core/broadcaster.py +++ b/pyinjective/core/broadcaster.py @@ -287,8 +287,8 @@ async def configure_gas_fee_for_transaction( class MessageBasedTransactionFeeCalculator(TransactionFeeCalculator): - DEFAULT_GAS_LIMIT = 400_000 - DEFAULT_EXCHANGE_GAS_LIMIT = 200_000 + DEFAULT_GAS_LIMIT = 150_000 + DEFAULT_EXCHANGE_GAS_LIMIT = 100_000 def __init__( self, @@ -313,11 +313,11 @@ def __init__( Decimal("2.5") * self._base_gas_limit ), ( - lambda message: "wasm" in self._message_type(message=message), + lambda message: "wasm." in self._message_type(message=message), Decimal("1.5") * self._base_gas_limit ), ( - lambda message: "exchange" in self._message_type(message=message), + lambda message: "exchange." in self._message_type(message=message), Decimal("1") * self._base_exchange_gas_limit ), ( @@ -353,7 +353,7 @@ def _message_type(self, message: any_pb2.Any) -> str: def _is_governance_message(self, message: any_pb2.Any) -> bool: message_type = self._message_type(message=message) - return "gov" in message_type and ("MsgDeposit" in message_type or "MsgSubmitProposal" in message_type) + return "gov." in message_type and ("MsgDeposit" in message_type or "MsgSubmitProposal" in message_type) def _calculate_gas_limit(self, messages: List[any_pb2.Any]) -> int: total_gas_limit = Decimal("0") From 2164c2cc647ca60c2a6f76672bd9dc80b5da4b8e Mon Sep 17 00:00:00 2001 From: abel Date: Fri, 18 Aug 2023 13:09:47 -0300 Subject: [PATCH 15/17] (fix) Undo change in the example to get a transaction by hash --- examples/exchange_client/explorer_rpc/1_GetTxByHash.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/exchange_client/explorer_rpc/1_GetTxByHash.py b/examples/exchange_client/explorer_rpc/1_GetTxByHash.py index fa21e293..7362d1e0 100644 --- a/examples/exchange_client/explorer_rpc/1_GetTxByHash.py +++ b/examples/exchange_client/explorer_rpc/1_GetTxByHash.py @@ -7,10 +7,10 @@ async def main() -> None: # select network: local, testnet, mainnet - network = Network.mainnet() + network = Network.testnet() client = AsyncClient(network, insecure=False) composer = Composer(network=network.string()) - tx_hash = "50DD80270052D85835939A393127B5626917007E71A7ABD5205F1A094B976C1B" + tx_hash = "0F3EBEC1882E1EEAC5B7BDD836E976250F1CD072B79485877CEACCB92ACDDF52" transaction_response = await client.get_tx_by_hash(tx_hash=tx_hash) print(transaction_response) From 6d3d7da0a28df1dfcd1f9cc22d77f67a0435e3f8 Mon Sep 17 00:00:00 2001 From: abel Date: Tue, 22 Aug 2023 10:20:04 -0300 Subject: [PATCH 16/17] (feat) Improved the logic in the gas limit estimator component to consider the cost of each included element for batch messages --- pyinjective/core/broadcaster.py | 60 +- pyinjective/core/gas_limit_estimator.py | 307 ++++++++++ tests/core/test_gas_limit_estimator.py | 527 ++++++++++++++++++ ...essage_based_transaction_fee_calculator.py | 50 +- 4 files changed, 866 insertions(+), 78 deletions(-) create mode 100644 pyinjective/core/gas_limit_estimator.py create mode 100644 tests/core/test_gas_limit_estimator.py diff --git a/pyinjective/core/broadcaster.py b/pyinjective/core/broadcaster.py index 55606b85..cc9dc2bb 100644 --- a/pyinjective/core/broadcaster.py +++ b/pyinjective/core/broadcaster.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod from decimal import Decimal -from typing import Callable, List, Optional, Tuple +from typing import List, Optional import math from google.protobuf import any_pb2 @@ -9,7 +9,7 @@ from pyinjective.async_client import AsyncClient from pyinjective.composer import Composer from pyinjective.constant import Network -from pyinjective.proto.cosmos.authz.v1beta1 import tx_pb2 as cosmos_authz_tx_pb +from pyinjective.core.gas_limit_estimator import GasLimitEstimator class BroadcasterAccountConfig(ABC): @@ -287,44 +287,16 @@ async def configure_gas_fee_for_transaction( class MessageBasedTransactionFeeCalculator(TransactionFeeCalculator): - DEFAULT_GAS_LIMIT = 150_000 - DEFAULT_EXCHANGE_GAS_LIMIT = 100_000 + TRANSACTION_GAS_LIMIT = 60_000 def __init__( self, client: AsyncClient, composer: Composer, - gas_price: Optional[int] = None, - base_gas_limit: Optional[int] = None, - base_exchange_gas_limit: Optional[int] = None): + gas_price: Optional[int] = None): self._client = client self._composer = composer self._gas_price = gas_price or self.DEFAULT_GAS_PRICE - self._base_gas_limit = base_gas_limit or self.DEFAULT_GAS_LIMIT - self._base_exchange_gas_limit = base_exchange_gas_limit or self.DEFAULT_EXCHANGE_GAS_LIMIT - - self._gas_limit_per_message_type_rules: List[Tuple[Callable, Decimal]] = [ - ( - lambda message: "MsgPrivilegedExecuteContract" in self._message_type(message=message), - Decimal("6") * self._base_gas_limit - ), - ( - lambda message: "MsgExecuteContract" in self._message_type(message=message), - Decimal("2.5") * self._base_gas_limit - ), - ( - lambda message: "wasm." in self._message_type(message=message), - Decimal("1.5") * self._base_gas_limit - ), - ( - lambda message: "exchange." in self._message_type(message=message), - Decimal("1") * self._base_exchange_gas_limit - ), - ( - lambda message: self._is_governance_message(message=message), - Decimal("15") * self._base_gas_limit - ), - ] async def configure_gas_fee_for_transaction( self, @@ -332,7 +304,8 @@ async def configure_gas_fee_for_transaction( private_key: PrivateKey, public_key: PublicKey, ): - transaction_gas_limit = math.ceil(self._calculate_gas_limit(messages=transaction.msgs)) + messages_gas_limit = math.ceil(self._calculate_gas_limit(messages=transaction.msgs)) + transaction_gas_limit = messages_gas_limit + self.TRANSACTION_GAS_LIMIT fee = [ self._composer.Coin( @@ -351,28 +324,11 @@ def _message_type(self, message: any_pb2.Any) -> str: message_type = message.DESCRIPTOR.full_name return message_type - def _is_governance_message(self, message: any_pb2.Any) -> bool: - message_type = self._message_type(message=message) - return "gov." in message_type and ("MsgDeposit" in message_type or "MsgSubmitProposal" in message_type) - def _calculate_gas_limit(self, messages: List[any_pb2.Any]) -> int: total_gas_limit = Decimal("0") for message in messages: - applying_rule = next( - (rule_tuple for rule_tuple in self._gas_limit_per_message_type_rules - if rule_tuple[0](message)), - None - ) - - if applying_rule is None: - total_gas_limit += self._base_gas_limit - else: - total_gas_limit += applying_rule[1] - - if self._message_type(message=message).endswith("MsgExec"): - exec_message = cosmos_authz_tx_pb.MsgExec.FromString(message.value) - sub_messages_limit = self._calculate_gas_limit(messages=exec_message.msgs) - total_gas_limit += sub_messages_limit + estimator = GasLimitEstimator.for_message(message=message) + total_gas_limit += estimator.gas_limit() return math.ceil(total_gas_limit) diff --git a/pyinjective/core/gas_limit_estimator.py b/pyinjective/core/gas_limit_estimator.py new file mode 100644 index 00000000..d21c385b --- /dev/null +++ b/pyinjective/core/gas_limit_estimator.py @@ -0,0 +1,307 @@ +from abc import ABC, abstractmethod + +from google.protobuf import any_pb2 + +from pyinjective.proto.cosmos.authz.v1beta1 import tx_pb2 as cosmos_authz_tx_pb +from pyinjective.proto.cosmos.gov.v1beta1 import tx_pb2 as gov_tx_pb +from pyinjective.proto.cosmwasm.wasm.v1 import tx_pb2 as wasm_tx_pb +from pyinjective.proto.injective.exchange.v1beta1 import tx_pb2 as injective_exchange_tx_pb + + +class GasLimitEstimator(ABC): + GENERAL_MESSAGE_GAS_LIMIT = 5_000 + BASIC_REFERENCE_GAS_LIMIT = 150_000 + + @classmethod + @abstractmethod + def applies_to(cls, message: any_pb2.Any) -> bool: + ... + + @classmethod + def for_message(cls, message: any_pb2.Any): + estimator_class = next((estimator_subclass for estimator_subclass in cls.__subclasses__() + if estimator_subclass.applies_to(message=message)), + None) + if estimator_class is None: + estimator = DefaultGasLimitEstimator() + else: + estimator = estimator_class(message=message) + + return estimator + + @abstractmethod + def gas_limit(self) -> int: + ... + + @staticmethod + def message_type(message: any_pb2.Any) -> str: + if isinstance(message, any_pb2.Any): + message_type = message.type_url + else: + message_type = message.DESCRIPTOR.full_name + return message_type + + @abstractmethod + def _message_class(self, message: any_pb2.Any): + ... + + def _parsed_message(self, message: any_pb2.Any) -> any_pb2.Any: + if isinstance(message, any_pb2.Any): + parsed_message = self._message_class(message=message).FromString(message.value) + else: + parsed_message = message + return parsed_message + + +class DefaultGasLimitEstimator(GasLimitEstimator): + DEFAULT_GAS_LIMIT = 150_000 + + @classmethod + def applies_to(cls, message: any_pb2.Any) -> bool: + return False + + def gas_limit(self) -> int: + return self.DEFAULT_GAS_LIMIT + + def _message_class(self, message: any_pb2.Any): + # This class should not try to convert messages + raise NotImplementedError + + +class BatchCreateSpotLimitOrdersGasLimitEstimator(GasLimitEstimator): + ORDER_GAS_LIMIT = 45_000 + + def __init__(self, message: any_pb2.Any): + self._message = self._parsed_message(message=message) + + @classmethod + def applies_to(cls, message: any_pb2.Any): + return cls.message_type(message=message).endswith("MsgBatchCreateSpotLimitOrders") + + def gas_limit(self) -> int: + total = 0 + total += self.GENERAL_MESSAGE_GAS_LIMIT + total += len(self._message.orders) * self.ORDER_GAS_LIMIT + + return total + + def _message_class(self, message: any_pb2.Any): + return injective_exchange_tx_pb.MsgBatchCreateSpotLimitOrders + + +class BatchCancelSpotOrdersGasLimitEstimator(GasLimitEstimator): + ORDER_GAS_LIMIT = 45_000 + + def __init__(self, message: any_pb2.Any): + self._message = self._parsed_message(message=message) + + @classmethod + def applies_to(cls, message: any_pb2.Any): + return cls.message_type(message=message).endswith("MsgBatchCancelSpotOrders") + + def gas_limit(self) -> int: + total = 0 + total += self.GENERAL_MESSAGE_GAS_LIMIT + total += len(self._message.data) * self.ORDER_GAS_LIMIT + + return total + + def _message_class(self, message: any_pb2.Any): + return injective_exchange_tx_pb.MsgBatchCancelSpotOrders + + +class BatchCreateDerivativeLimitOrdersGasLimitEstimator(GasLimitEstimator): + ORDER_GAS_LIMIT = 60_000 + + def __init__(self, message: any_pb2.Any): + self._message = self._parsed_message(message=message) + + @classmethod + def applies_to(cls, message: any_pb2.Any): + return cls.message_type(message=message).endswith("MsgBatchCreateDerivativeLimitOrders") + + def gas_limit(self) -> int: + total = 0 + total += self.GENERAL_MESSAGE_GAS_LIMIT + total += len(self._message.orders) * self.ORDER_GAS_LIMIT + + return total + + def _message_class(self, message: any_pb2.Any): + return injective_exchange_tx_pb.MsgBatchCreateDerivativeLimitOrders + + +class BatchCancelDerivativeOrdersGasLimitEstimator(GasLimitEstimator): + ORDER_GAS_LIMIT = 55_000 + + def __init__(self, message: any_pb2.Any): + self._message = self._parsed_message(message=message) + + @classmethod + def applies_to(cls, message: any_pb2.Any): + return cls.message_type(message=message).endswith("MsgBatchCancelDerivativeOrders") + + def gas_limit(self) -> int: + total = 0 + total += self.GENERAL_MESSAGE_GAS_LIMIT + total += len(self._message.data) * self.ORDER_GAS_LIMIT + + return total + + def _message_class(self, message: any_pb2.Any): + return injective_exchange_tx_pb.MsgBatchCancelDerivativeOrders + + +class BatchUpdateOrdersGasLimitEstimator(GasLimitEstimator): + SPOT_ORDER_CREATION_GAS_LIMIT = 40_000 + DERIVATIVE_ORDER_CREATION_GAS_LIMIT = 60_000 + SPOT_ORDER_CANCELATION_GAS_LIMIT = 45_000 + DERIVATIVE_ORDER_CANCELATION_GAS_LIMIT = 55_000 + CANCEL_ALL_SPOT_MARKET_GAS_LIMIT = 35_000 + CANCEL_ALL_DERIVATIVE_MARKET_GAS_LIMIT = 45_000 + MESSAGE_GAS_LIMIT = 10_000 + + AVERAGE_CANCEL_ALL_AFFECTED_ORDERS = 20 + + def __init__(self, message: any_pb2.Any): + self._message = self._parsed_message(message=message) + + @classmethod + def applies_to(cls, message: any_pb2.Any): + return cls.message_type(message=message).endswith("MsgBatchUpdateOrders") + + def gas_limit(self) -> int: + total = 0 + total += self.MESSAGE_GAS_LIMIT + total += len(self._message.spot_orders_to_create) * self.SPOT_ORDER_CREATION_GAS_LIMIT + total += len(self._message.derivative_orders_to_create) * self.DERIVATIVE_ORDER_CREATION_GAS_LIMIT + total += len(self._message.binary_options_orders_to_create) * self.DERIVATIVE_ORDER_CREATION_GAS_LIMIT + total += len(self._message.spot_orders_to_cancel) * self.SPOT_ORDER_CANCELATION_GAS_LIMIT + total += len(self._message.derivative_orders_to_cancel) * self.DERIVATIVE_ORDER_CANCELATION_GAS_LIMIT + total += len(self._message.binary_options_orders_to_cancel) * self.DERIVATIVE_ORDER_CANCELATION_GAS_LIMIT + + total += (len(self._message.spot_market_ids_to_cancel_all) + * self.CANCEL_ALL_SPOT_MARKET_GAS_LIMIT + * self.AVERAGE_CANCEL_ALL_AFFECTED_ORDERS) + total += (len(self._message.derivative_market_ids_to_cancel_all) + * self.CANCEL_ALL_DERIVATIVE_MARKET_GAS_LIMIT + * self.AVERAGE_CANCEL_ALL_AFFECTED_ORDERS) + total += (len(self._message.binary_options_market_ids_to_cancel_all) + * self.CANCEL_ALL_DERIVATIVE_MARKET_GAS_LIMIT + * self.AVERAGE_CANCEL_ALL_AFFECTED_ORDERS) + + return total + + def _message_class(self, message: any_pb2.Any): + return injective_exchange_tx_pb.MsgBatchUpdateOrders + + +class ExecGasLimitEstimator(GasLimitEstimator): + DEFAULT_GAS_LIMIT = 5_000 + + def __init__(self, message: any_pb2.Any): + self._message = self._parsed_message(message=message) + + @classmethod + def applies_to(cls, message: any_pb2.Any) -> bool: + return cls.message_type(message=message).endswith("MsgExec") + + def gas_limit(self) -> int: + total = sum([GasLimitEstimator.for_message(message=inner_message).gas_limit() + for inner_message in self._message.msgs]) + total += self.DEFAULT_GAS_LIMIT + + return total + + def _message_class(self, message: any_pb2.Any): + return cosmos_authz_tx_pb.MsgExec + + +class PrivilegedExecuteContractGasLimitEstimator(GasLimitEstimator): + + def __init__(self, message: any_pb2.Any): + self._message = self._parsed_message(message=message) + + @classmethod + def applies_to(cls, message: any_pb2.Any) -> bool: + return cls.message_type(message=message).endswith("MsgPrivilegedExecuteContract") + + def gas_limit(self) -> int: + return self.BASIC_REFERENCE_GAS_LIMIT * 6 + + def _message_class(self, message: any_pb2.Any): + return injective_exchange_tx_pb.MsgPrivilegedExecuteContract + + +class ExecuteContractGasLimitEstimator(GasLimitEstimator): + + def __init__(self, message: any_pb2.Any): + self._message = self._parsed_message(message=message) + + @classmethod + def applies_to(cls, message: any_pb2.Any) -> bool: + return cls.message_type(message=message).endswith("MsgExecuteContract") + + def gas_limit(self) -> int: + return int(self.BASIC_REFERENCE_GAS_LIMIT * 2.5) + + def _message_class(self, message: any_pb2.Any): + return wasm_tx_pb.MsgExecuteContract + + +class GeneralWasmGasLimitEstimator(GasLimitEstimator): + + def __init__(self, message: any_pb2.Any): + self._message = self._parsed_message(message=message) + + @classmethod + def applies_to(cls, message: any_pb2.Any) -> bool: + return "wasm." in cls.message_type(message=message) + + def gas_limit(self) -> int: + return int(self.BASIC_REFERENCE_GAS_LIMIT * 1.5) + + def _message_class(self, message: any_pb2.Any): + return wasm_tx_pb.MsgInstantiateContract2 + + +class GovernanceGasLimitEstimator(GasLimitEstimator): + + def __init__(self, message: any_pb2.Any): + self._message = self._parsed_message(message=message) + + @classmethod + def applies_to(cls, message: any_pb2.Any) -> bool: + message_type = cls.message_type(message=message) + return "gov." in message_type and ( + message_type.endswith("MsgDeposit") or message_type.endswith("MsgSubmitProposal") + ) + + def gas_limit(self) -> int: + return int(self.BASIC_REFERENCE_GAS_LIMIT * 15) + + def _message_class(self, message: any_pb2.Any): + if "MsgDeposit" in self.message_type(message=message): + message_class = gov_tx_pb.MsgDeposit + else: + message_class = gov_tx_pb.MsgSubmitProposal + return message_class + + +class GenericExchangeGasLimitEstimator(GasLimitEstimator): + BASIC_REFERENCE_GAS_LIMIT = 100_000 + + def __init__(self, message: any_pb2.Any): + self._message = message + + @classmethod + def applies_to(cls, message: any_pb2.Any) -> bool: + message_type = cls.message_type(message=message) + return "exchange." in message_type + + def gas_limit(self) -> int: + return self.BASIC_REFERENCE_GAS_LIMIT + + def _message_class(self, message: any_pb2.Any): + # This class applies to many different messages, but we don't need to transform from Any format + raise NotImplementedError diff --git a/tests/core/test_gas_limit_estimator.py b/tests/core/test_gas_limit_estimator.py new file mode 100644 index 00000000..070ebb26 --- /dev/null +++ b/tests/core/test_gas_limit_estimator.py @@ -0,0 +1,527 @@ +from pyinjective.composer import Composer +from pyinjective.core.gas_limit_estimator import GasLimitEstimator +from pyinjective.proto.cosmos.gov.v1beta1 import tx_pb2 as gov_tx_pb +from pyinjective.proto.cosmwasm.wasm.v1 import tx_pb2 as wasm_tx_pb +from pyinjective.proto.injective.exchange.v1beta1 import tx_pb2 as injective_exchange_tx_pb + + +class TestGasLimitEstimator: + + def test_estimation_for_message_without_applying_rule(self): + composer = Composer(network="testnet") + message = composer.MsgSend( + from_address="from_address", + to_address="to_address", + amount=1, + denom='INJ' + ) + + estimator = GasLimitEstimator.for_message(message=message) + + expected_message_gas_limit = 150_000 + + assert(expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_batch_create_spot_limit_orders(self): + spot_market_id = "0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe" + composer = Composer(network="testnet") + orders = [ + composer.SpotOrder( + market_id=spot_market_id, + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=5, + quantity=1, + is_buy=True, + is_po=False + ), + composer.SpotOrder( + market_id=spot_market_id, + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=4, + quantity=1, + is_buy=True, + is_po=False + ), + ] + message = composer.MsgBatchCreateSpotLimitOrders( + sender="sender", + orders=orders + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_order_gas_limit = 45000 + expected_message_gas_limit = 5000 + + assert((expected_order_gas_limit * 2) + expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_batch_cancel_spot_orders(self): + spot_market_id = "0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe" + composer = Composer(network="testnet") + orders = [ + composer.OrderData( + market_id=spot_market_id, + subaccount_id="subaccount_id", + order_hash="0x3870fbdd91f07d54425147b1bb96404f4f043ba6335b422a6d494d285b387f2d" + ), + composer.OrderData( + market_id=spot_market_id, + subaccount_id="subaccount_id", + order_hash="0x222daa22f60fe9f075ed0ca583459e121c23e64431c3fbffdedda04598ede0d2" + ), + composer.OrderData( + market_id=spot_market_id, + subaccount_id="subaccount_id", + order_hash="0x7ee76255d7ca763c56b0eab9828fca89fdd3739645501c8a80f58b62b4f76da5" + ) + ] + message = composer.MsgBatchCancelSpotOrders( + sender="sender", + data=orders + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_order_gas_limit = 45000 + expected_message_gas_limit = 5000 + + assert((expected_order_gas_limit * 3) + expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_batch_create_derivative_limit_orders(self): + market_id = "0x17ef48032cb24375ba7c2e39f384e56433bcab20cbee9a7357e4cba2eb00abe6" + composer = Composer(network="testnet") + orders = [ + composer.DerivativeOrder( + market_id=market_id, + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=3, + quantity=1, + leverage=1, + is_buy=True, + is_po=False + ), + composer.DerivativeOrder( + market_id=market_id, + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=20, + quantity=1, + leverage=1, + is_buy=False, + is_reduce_only=False + ), + ] + message = composer.MsgBatchCreateDerivativeLimitOrders( + sender="sender", + orders=orders + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_order_gas_limit = 60_000 + expected_message_gas_limit = 5000 + + assert((expected_order_gas_limit * 2) + expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_batch_cancel_derivative_orders(self): + spot_market_id = "0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe" + composer = Composer(network="testnet") + orders = [ + composer.OrderData( + market_id=spot_market_id, + subaccount_id="subaccount_id", + order_hash="0x3870fbdd91f07d54425147b1bb96404f4f043ba6335b422a6d494d285b387f2d" + ), + composer.OrderData( + market_id=spot_market_id, + subaccount_id="subaccount_id", + order_hash="0x222daa22f60fe9f075ed0ca583459e121c23e64431c3fbffdedda04598ede0d2" + ), + composer.OrderData( + market_id=spot_market_id, + subaccount_id="subaccount_id", + order_hash="0x7ee76255d7ca763c56b0eab9828fca89fdd3739645501c8a80f58b62b4f76da5" + ) + ] + message = composer.MsgBatchCancelDerivativeOrders( + sender="sender", + data=orders + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_order_gas_limit = 55_000 + expected_message_gas_limit = 5000 + + assert((expected_order_gas_limit * 3) + expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_batch_update_orders_to_create_spot_orders(self): + market_id = "0x17ef48032cb24375ba7c2e39f384e56433bcab20cbee9a7357e4cba2eb00abe6" + composer = Composer(network="testnet") + orders = [ + composer.SpotOrder( + market_id=market_id, + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=5, + quantity=1, + is_buy=True, + is_po=False + ), + composer.SpotOrder( + market_id=market_id, + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=4, + quantity=1, + is_buy=True, + is_po=False + ), + ] + message = composer.MsgBatchUpdateOrders( + sender="senders", + derivative_orders_to_create=[], + spot_orders_to_create=orders, + derivative_orders_to_cancel=[], + spot_orders_to_cancel=[] + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_order_gas_limit = 40_000 + expected_message_gas_limit = 10_000 + + assert((expected_order_gas_limit * 2) + expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_batch_update_orders_to_create_derivative_orders(self): + market_id = "0x17ef48032cb24375ba7c2e39f384e56433bcab20cbee9a7357e4cba2eb00abe6" + composer = Composer(network="testnet") + orders = [ + composer.DerivativeOrder( + market_id=market_id, + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=3, + quantity=1, + leverage=1, + is_buy=True, + is_po=False + ), + composer.DerivativeOrder( + market_id=market_id, + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=20, + quantity=1, + leverage=1, + is_buy=False, + is_reduce_only=False + ), + ] + message = composer.MsgBatchUpdateOrders( + sender="senders", + derivative_orders_to_create=orders, + spot_orders_to_create=[], + derivative_orders_to_cancel=[], + spot_orders_to_cancel=[] + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_order_gas_limit = 60_000 + expected_message_gas_limit = 10_000 + + assert((expected_order_gas_limit * 2) + expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_batch_update_orders_to_create_binary_orders(self): + market_id = "0x17ef48032cb24375ba7c2e39f384e56433bcab20cbee9a7357e4cba2eb00abe6" + composer = Composer(network="testnet") + orders = [ + composer.BinaryOptionsOrder( + market_id=market_id, + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=3, + quantity=1, + leverage=1, + is_buy=True, + is_po=False + ), + composer.BinaryOptionsOrder( + market_id=market_id, + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=20, + quantity=1, + leverage=1, + is_buy=False, + is_reduce_only=False + ), + ] + message = composer.MsgBatchUpdateOrders( + sender="senders", + derivative_orders_to_create=[], + spot_orders_to_create=[], + binary_options_orders_to_create=orders, + derivative_orders_to_cancel=[], + spot_orders_to_cancel=[] + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_order_gas_limit = 60_000 + expected_message_gas_limit = 10_000 + + assert((expected_order_gas_limit * 2) + expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_batch_update_orders_to_cancel_spot_orders(self): + market_id = "0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe" + composer = Composer(network="testnet") + orders = [ + composer.OrderData( + market_id=market_id, + subaccount_id="subaccount_id", + order_hash="0x3870fbdd91f07d54425147b1bb96404f4f043ba6335b422a6d494d285b387f2d" + ), + composer.OrderData( + market_id=market_id, + subaccount_id="subaccount_id", + order_hash="0x222daa22f60fe9f075ed0ca583459e121c23e64431c3fbffdedda04598ede0d2" + ), + composer.OrderData( + market_id=market_id, + subaccount_id="subaccount_id", + order_hash="0x7ee76255d7ca763c56b0eab9828fca89fdd3739645501c8a80f58b62b4f76da5" + ) + ] + message = composer.MsgBatchUpdateOrders( + sender="senders", + derivative_orders_to_create=[], + spot_orders_to_create=[], + derivative_orders_to_cancel=[], + spot_orders_to_cancel=orders + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_order_gas_limit = 45_000 + expected_message_gas_limit = 10_000 + + assert((expected_order_gas_limit * 3) + expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_batch_update_orders_to_cancel_derivative_orders(self): + market_id = "0x17ef48032cb24375ba7c2e39f384e56433bcab20cbee9a7357e4cba2eb00abe6" + composer = Composer(network="testnet") + orders = [ + composer.OrderData( + market_id=market_id, + subaccount_id="subaccount_id", + order_hash="0x3870fbdd91f07d54425147b1bb96404f4f043ba6335b422a6d494d285b387f2d" + ), + composer.OrderData( + market_id=market_id, + subaccount_id="subaccount_id", + order_hash="0x222daa22f60fe9f075ed0ca583459e121c23e64431c3fbffdedda04598ede0d2" + ), + composer.OrderData( + market_id=market_id, + subaccount_id="subaccount_id", + order_hash="0x7ee76255d7ca763c56b0eab9828fca89fdd3739645501c8a80f58b62b4f76da5" + ) + ] + message = composer.MsgBatchUpdateOrders( + sender="senders", + derivative_orders_to_create=[], + spot_orders_to_create=[], + derivative_orders_to_cancel=orders, + spot_orders_to_cancel=[] + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_order_gas_limit = 55_000 + expected_message_gas_limit = 10_000 + + assert((expected_order_gas_limit * 3) + expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_batch_update_orders_to_cancel_binary_orders(self): + market_id = "0x17ef48032cb24375ba7c2e39f384e56433bcab20cbee9a7357e4cba2eb00abe6" + composer = Composer(network="testnet") + orders = [ + composer.OrderData( + market_id=market_id, + subaccount_id="subaccount_id", + order_hash="0x3870fbdd91f07d54425147b1bb96404f4f043ba6335b422a6d494d285b387f2d" + ), + composer.OrderData( + market_id=market_id, + subaccount_id="subaccount_id", + order_hash="0x222daa22f60fe9f075ed0ca583459e121c23e64431c3fbffdedda04598ede0d2" + ), + composer.OrderData( + market_id=market_id, + subaccount_id="subaccount_id", + order_hash="0x7ee76255d7ca763c56b0eab9828fca89fdd3739645501c8a80f58b62b4f76da5" + ) + ] + message = composer.MsgBatchUpdateOrders( + sender="senders", + derivative_orders_to_create=[], + spot_orders_to_create=[], + derivative_orders_to_cancel=[], + spot_orders_to_cancel=[], + binary_options_orders_to_cancel=orders, + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_order_gas_limit = 55_000 + expected_message_gas_limit = 10_000 + + assert((expected_order_gas_limit * 3) + expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_batch_update_orders_to_cancel_all_for_spot_market(self): + market_id = "0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe" + composer = Composer(network="testnet") + + message = composer.MsgBatchUpdateOrders( + sender="senders", + subaccount_id="subaccount_id", + spot_market_ids_to_cancel_all=[market_id], + derivative_orders_to_create=[], + spot_orders_to_create=[], + derivative_orders_to_cancel=[], + spot_orders_to_cancel=[] + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_gas_limit = 35_000 * 20 + expected_message_gas_limit = 10_000 + + assert(expected_gas_limit + expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_batch_update_orders_to_cancel_all_for_derivative_market(self): + market_id = "0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe" + composer = Composer(network="testnet") + + message = composer.MsgBatchUpdateOrders( + sender="senders", + subaccount_id="subaccount_id", + derivative_market_ids_to_cancel_all=[market_id], + derivative_orders_to_create=[], + spot_orders_to_create=[], + derivative_orders_to_cancel=[], + spot_orders_to_cancel=[] + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_gas_limit = 45_000 * 20 + expected_message_gas_limit = 10_000 + + assert(expected_gas_limit + expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_batch_update_orders_to_cancel_all_for_binary_options_market(self): + market_id = "0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe" + composer = Composer(network="testnet") + + message = composer.MsgBatchUpdateOrders( + sender="senders", + subaccount_id="subaccount_id", + binary_options_market_ids_to_cancel_all=[market_id], + derivative_orders_to_create=[], + spot_orders_to_create=[], + derivative_orders_to_cancel=[], + spot_orders_to_cancel=[] + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_gas_limit = 45_000 * 20 + expected_message_gas_limit = 10_000 + + assert(expected_gas_limit + expected_message_gas_limit == estimator.gas_limit()) + + def test_estimation_for_exec_message(self): + market_id = "0x17ef48032cb24375ba7c2e39f384e56433bcab20cbee9a7357e4cba2eb00abe6" + composer = Composer(network="testnet") + orders = [ + composer.SpotOrder( + market_id=market_id, + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=5, + quantity=1, + is_buy=True, + is_po=False + ), + ] + inner_message = composer.MsgBatchUpdateOrders( + sender="senders", + derivative_orders_to_create=[], + spot_orders_to_create=orders, + derivative_orders_to_cancel=[], + spot_orders_to_cancel=[] + ) + message = composer.MsgExec( + grantee="grantee", + msgs=[inner_message] + ) + + estimator = GasLimitEstimator.for_message(message=message) + + expected_order_gas_limit = 40_000 + expected_inner_message_gas_limit = 10_000 + expected_exec_message_gas_limit = 5_000 + + assert(expected_order_gas_limit + + expected_inner_message_gas_limit + + expected_exec_message_gas_limit + == estimator.gas_limit()) + + def test_estimation_for_privileged_execute_contract_message(self): + message = injective_exchange_tx_pb.MsgPrivilegedExecuteContract() + estimator = GasLimitEstimator.for_message(message=message) + + expected_gas_limit = 900_000 + + assert(expected_gas_limit == estimator.gas_limit()) + + + def test_estimation_for_execute_contract_message(self): + composer = Composer(network="testnet") + message = composer.MsgExecuteContract( + sender="", + contract="", + msg="", + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_gas_limit = 375_000 + + assert(expected_gas_limit == estimator.gas_limit()) + + def test_estimation_for_wasm_message(self): + message = wasm_tx_pb.MsgInstantiateContract2() + estimator = GasLimitEstimator.for_message(message=message) + + expected_gas_limit = 225_000 + + assert(expected_gas_limit == estimator.gas_limit()) + + def test_estimation_for_governance_message(self): + message = gov_tx_pb.MsgDeposit() + estimator = GasLimitEstimator.for_message(message=message) + + expected_gas_limit = 2_250_000 + + assert(expected_gas_limit == estimator.gas_limit()) + + def test_estimation_for_generic_exchange_message(self): + composer = Composer(network="testnet") + message = composer.MsgCreateSpotLimitOrder( + sender="sender", + market_id="0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe", + subaccount_id="subaccount_id", + fee_recipient="fee_recipient", + price=7.523, + quantity=0.01, + is_buy=True, + is_po=False + ) + estimator = GasLimitEstimator.for_message(message=message) + + expected_gas_limit = 100_000 + + assert(expected_gas_limit == estimator.gas_limit()) diff --git a/tests/core/test_message_based_transaction_fee_calculator.py b/tests/core/test_message_based_transaction_fee_calculator.py index 14bfea81..602b71af 100644 --- a/tests/core/test_message_based_transaction_fee_calculator.py +++ b/tests/core/test_message_based_transaction_fee_calculator.py @@ -24,8 +24,6 @@ async def test_gas_fee_for_privileged_execute_contract_message(self): client=client, composer=composer, gas_price=5_000_000, - base_gas_limit=400_000, - base_exchange_gas_limit=150_000, ) message = tx_pb2.MsgPrivilegedExecuteContract() @@ -34,7 +32,8 @@ async def test_gas_fee_for_privileged_execute_contract_message(self): await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) - expected_gas_limit = math.ceil(Decimal(6) * 400_000) + expected_transaction_gas_limit = 60_000 + expected_gas_limit = math.ceil(Decimal(6) * 150_000 + expected_transaction_gas_limit) assert(expected_gas_limit == transaction.fee.gas_limit) assert(str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) @@ -47,8 +46,6 @@ async def test_gas_fee_for_execute_contract_message(self): client=client, composer=composer, gas_price=5_000_000, - base_gas_limit=400_000, - base_exchange_gas_limit=150_000, ) message = composer.MsgExecuteContract( @@ -61,7 +58,8 @@ async def test_gas_fee_for_execute_contract_message(self): await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) - expected_gas_limit = math.ceil(Decimal(2.5) * 400_000) + expected_transaction_gas_limit = 60_000 + expected_gas_limit = math.ceil(Decimal(2.5) * 150_000 + expected_transaction_gas_limit) assert (expected_gas_limit == transaction.fee.gas_limit) assert (str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) @@ -74,8 +72,6 @@ async def test_gas_fee_for_wasm_message(self): client=client, composer=composer, gas_price=5_000_000, - base_gas_limit=400_000, - base_exchange_gas_limit=150_000, ) message = wasm_tx_pb2.MsgInstantiateContract2() @@ -84,7 +80,8 @@ async def test_gas_fee_for_wasm_message(self): await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) - expected_gas_limit = math.ceil(Decimal(1.5) * 400_000) + expected_transaction_gas_limit = 60_000 + expected_gas_limit = math.ceil(Decimal(1.5) * 150_000 + expected_transaction_gas_limit) assert (expected_gas_limit == transaction.fee.gas_limit) assert (str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) @@ -97,8 +94,6 @@ async def test_gas_fee_for_governance_message(self): client=client, composer=composer, gas_price=5_000_000, - base_gas_limit=400_000, - base_exchange_gas_limit=150_000, ) message = gov_tx_pb2.MsgDeposit() @@ -107,7 +102,8 @@ async def test_gas_fee_for_governance_message(self): await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) - expected_gas_limit = math.ceil(Decimal(15) * 400_000) + expected_transaction_gas_limit = 60_000 + expected_gas_limit = math.ceil(Decimal(15) * 150_000 + expected_transaction_gas_limit) assert (expected_gas_limit == transaction.fee.gas_limit) assert (str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) @@ -120,8 +116,6 @@ async def test_gas_fee_for_exchange_message(self): client=client, composer=composer, gas_price=5_000_000, - base_gas_limit=400_000, - base_exchange_gas_limit=150_000, ) message = composer.MsgCreateSpotLimitOrder( @@ -139,7 +133,8 @@ async def test_gas_fee_for_exchange_message(self): await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) - expected_gas_limit = math.ceil(Decimal(1) * 150_000) + expected_transaction_gas_limit = 60_000 + expected_gas_limit = math.ceil(Decimal(1) * 100_000 + expected_transaction_gas_limit) assert (expected_gas_limit == transaction.fee.gas_limit) assert (str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) @@ -152,8 +147,6 @@ async def test_gas_fee_for_msg_exec_message(self): client=client, composer=composer, gas_price=5_000_000, - base_gas_limit=400_000, - base_exchange_gas_limit=150_000, ) inner_message = composer.MsgCreateSpotLimitOrder( @@ -175,9 +168,12 @@ async def test_gas_fee_for_msg_exec_message(self): await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) - expected_inner_message_gas_limit = Decimal(1) * 150_000 - expected_exec_message_gas_limit = 400_000 - expected_gas_limit = math.ceil(expected_exec_message_gas_limit + expected_inner_message_gas_limit) + expected_transaction_gas_limit = 60_000 + expected_inner_message_gas_limit = Decimal(1) * 100_000 + expected_exec_message_gas_limit = 5_000 + expected_gas_limit = math.ceil( + expected_exec_message_gas_limit + expected_inner_message_gas_limit + expected_transaction_gas_limit + ) assert (expected_gas_limit == transaction.fee.gas_limit) assert (str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) @@ -190,8 +186,6 @@ async def test_gas_fee_for_two_messages_in_one_transaction(self): client=client, composer=composer, gas_price=5_000_000, - base_gas_limit=400_000, - base_exchange_gas_limit=150_000, ) inner_message = composer.MsgCreateSpotLimitOrder( @@ -221,11 +215,15 @@ async def test_gas_fee_for_two_messages_in_one_transaction(self): await calculator.configure_gas_fee_for_transaction(transaction=transaction, private_key=None, public_key=None) - expected_inner_message_gas_limit = Decimal(1) * 150_000 - expected_exec_message_gas_limit = 400_000 - expected_send_message_gas_limit = 400_000 + expected_transaction_gas_limit = 60_000 + expected_inner_message_gas_limit = Decimal(1) * 100_000 + expected_exec_message_gas_limit = 5_000 + expected_send_message_gas_limit = 150_000 expected_gas_limit = math.ceil( - expected_exec_message_gas_limit + expected_inner_message_gas_limit + expected_send_message_gas_limit + expected_exec_message_gas_limit + + expected_inner_message_gas_limit + + expected_send_message_gas_limit + + expected_transaction_gas_limit ) assert (expected_gas_limit == transaction.fee.gas_limit) assert (str(expected_gas_limit * 5_000_000) == transaction.fee.amount[0].amount) From 4a65fd80a16158ec6b8efd704614013a21d8a48b Mon Sep 17 00:00:00 2001 From: abel Date: Sat, 26 Aug 2023 21:10:21 -0300 Subject: [PATCH 17/17] (fix) Synchronized the denoms config files for mainnet and testnet --- README.md | 3 +++ pyinjective/denoms_mainnet.ini | 19 +++++-------------- pyinjective/denoms_testnet.ini | 20 +------------------- setup.py | 2 +- 4 files changed, 10 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 67fa1bd4..8c2ec6e2 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,9 @@ make tests ``` ### Changelogs +**0.7.2.1** +* Synchronization of denoms configuration files. + **0.7.2** * Added a new gas limit calculation for the TransactionBroadcaster that estimates the value based on the messages in the transaction (without running the transaction simulation). diff --git a/pyinjective/denoms_mainnet.ini b/pyinjective/denoms_mainnet.ini index b6e0080f..bf7b610b 100644 --- a/pyinjective/denoms_mainnet.ini +++ b/pyinjective/denoms_mainnet.ini @@ -200,7 +200,7 @@ min_display_quantity_tick_size = 0.01 description = 'Mainnet Spot GF/USDT' base = 18 quote = 6 -min_price_tick_size = 0.000000000000001 +min_price_tick_size = 0.0000000000000001 min_display_price_tick_size = 0.001 min_quantity_tick_size = 1000000000000000 min_display_quantity_tick_size = 0.001 @@ -317,7 +317,7 @@ min_display_quantity_tick_size = 100.0 description = 'Mainnet Spot STRD/USDT' base = 6 quote = 6 -min_price_tick_size = 0.001 +min_price_tick_size = 0.0001 min_display_price_tick_size = 1e-09 min_quantity_tick_size = 1000 min_display_quantity_tick_size = 1000.0 @@ -328,7 +328,7 @@ base = 6 quote = 6 min_price_tick_size = 0.0001 min_display_price_tick_size = 0.0001 -min_quantity_tick_size = 10000000 +min_quantity_tick_size = 1000000 min_display_quantity_tick_size = 10.0 [0x4fa0bd2c2adbfe077f58395c18a72f5cbf89532743e3bddf43bc7aba706b0b74] @@ -452,7 +452,7 @@ min_display_quantity_tick_size = 0.01 description = 'Mainnet Derivative STX/USDT PERP' base = 0 quote = 6 -min_price_tick_size = 1000 +min_price_tick_size = 100 min_display_price_tick_size = 0.001 min_quantity_tick_size = 0.1 min_display_quantity_tick_size = 0.1 @@ -493,15 +493,6 @@ min_display_price_tick_size = 0.01 min_quantity_tick_size = 0.1 min_display_quantity_tick_size = 0.1 -[0x5e7be7948c78f0c7fb1170655b5faa0a519ee0801250dde0b50308791474e61c] -description = 'Mainnet Derivative ETH/USDT 19SEP22' -base = 0 -quote = 6 -min_price_tick_size = 100000 -min_display_price_tick_size = 0.1 -min_quantity_tick_size = 0.01 -min_display_quantity_tick_size = 0.01 - [0x3b7fb1d9351f7fa2e6e0e5a11b3639ee5e0486c33a6a74f629c3fc3c3043efd5] description = 'Mainnet Derivative BONK/USDT PERP' base = 0 @@ -682,7 +673,7 @@ decimals = 8 peggy_denom = factory/inj14ejqjyq8um4p3xfqj74yld5waqljf88f9eneuk/inj1d5vz0uzwlpfvgwrwulxg6syy82axa58y4fuszd decimals = 8 -[wMATIC] +[WMATIC] peggy_denom = factory/inj14ejqjyq8um4p3xfqj74yld5waqljf88f9eneuk/inj1dxv423h8ygzgxmxnvrf33ws3k94aedfdevxd8h decimals = 8 diff --git a/pyinjective/denoms_testnet.ini b/pyinjective/denoms_testnet.ini index 45a2e6dc..2e24698f 100644 --- a/pyinjective/denoms_testnet.ini +++ b/pyinjective/denoms_testnet.ini @@ -56,25 +56,7 @@ min_display_quantity_tick_size = 0.1 description = 'Testnet Derivative INJ/USDT PERP' base = 0 quote = 6 -min_price_tick_size = 100000 -min_display_price_tick_size = 0.1 -min_quantity_tick_size = 0.0001 -min_display_quantity_tick_size = 0.0001 - -[0xd5e4b12b19ecf176e4e14b42944731c27677819d2ed93be4104ad7025529c7ff] -description = 'Testnet Derivative ETH/USDT PERP' -base = 0 -quote = 6 -min_price_tick_size = 100000 -min_display_price_tick_size = 0.1 -min_quantity_tick_size = 0.0001 -min_display_quantity_tick_size = 0.0001 - -[0x90e662193fa29a3a7e6c07be4407c94833e762d9ee82136a2cc712d6b87d7de3] -description = 'Testnet Derivative BTC/USDT PERP' -base = 0 -quote = 6 -min_price_tick_size = 100000 +min_price_tick_size = 100 min_display_price_tick_size = 0.1 min_quantity_tick_size = 0.0001 min_display_quantity_tick_size = 0.0001 diff --git a/setup.py b/setup.py index 8481af42..f8275c57 100755 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ EMAIL = "achilleas@injectivelabs.com" AUTHOR = "Injective Labs" REQUIRES_PYTHON = ">=3.9" -VERSION = "0.7.2" +VERSION = "0.7.2.1" REQUIRED = [ "aiohttp",