diff --git a/Makefile b/Makefile index 94f55d2a..2485ee70 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,6 @@ copy-proto: done tests: - pytest -v tests/** + pytest -v .PHONY: all gen gen-client copy-proto tests diff --git a/Pipfile b/Pipfile index f07ae931..97f04274 100644 --- a/Pipfile +++ b/Pipfile @@ -25,6 +25,7 @@ websockets = "*" [dev-packages] pytest = "*" pytest-asyncio = "*" +pytest-grpc = "*" requests-mock = "*" [requires] diff --git a/Pipfile.lock b/Pipfile.lock index 4865929d..ee240533 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ddd9c0acafbf86fc3b26c88650bee0a2aa19d05a537541bfcbd8c8c2a1401266" + "sha256": "414c3e95a8abb1f4986d0ae4814e25f8c7eba47e5343b5c67cf0bab32b21eadc" }, "pipfile-spec": 6, "requires": { @@ -1518,6 +1518,14 @@ "index": "pypi", "version": "==0.21.0" }, + "pytest-grpc": { + "hashes": [ + "sha256:0bd2683ffd34199444d707c0ab01970b22e0afbba6cb1ddb6d578c85ebfe09bd", + "sha256:5b062cf498e59995e84b3051da76f7bcff8cfe307927869f7bdc27ab967eee35" + ], + "index": "pypi", + "version": "==0.8.0" + }, "requests": { "hashes": [ "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", diff --git a/README.md b/README.md index fbf07b17..cb0daa27 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,12 @@ make tests ``` ### Changelogs +**0.7.1**(change before release) +* Refactor Composer to be created with all the markets and tokens. The Composer now uses the real markets and tokens to convert human-readable values to chain format +* The Composer can still be instantiated without markets and tokens. When markets and tokens are not provided the Composer loads the required information from the Denoms used in previous versions +* Change in AsyncClient to be able to create Composer instances for the client network, markets and tokens +* Examples have been adapted to create Composer instances using the AsyncClient + **0.7** * Removed references to pysha3 library (and also eip712-struct that required it) and replaced it with other implementation to allow the project to work with Python 3.11 * Updated sentry nodes LCD URL, for each sentry node to use its own service diff --git a/examples/chain_client/0_LocalOrderHash.py b/examples/chain_client/0_LocalOrderHash.py index a43cffee..12f6e448 100644 --- a/examples/chain_client/0_LocalOrderHash.py +++ b/examples/chain_client/0_LocalOrderHash.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -11,10 +9,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account diff --git a/examples/chain_client/13_MsgIncreasePositionMargin.py b/examples/chain_client/13_MsgIncreasePositionMargin.py index 6d3926ec..1f89b287 100644 --- a/examples/chain_client/13_MsgIncreasePositionMargin.py +++ b/examples/chain_client/13_MsgIncreasePositionMargin.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -11,10 +9,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account diff --git a/examples/chain_client/15_MsgWithdraw.py b/examples/chain_client/15_MsgWithdraw.py index 74fc8bce..5694ae46 100644 --- a/examples/chain_client/15_MsgWithdraw.py +++ b/examples/chain_client/15_MsgWithdraw.py @@ -13,9 +13,7 @@ # limitations under the License. import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -25,10 +23,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account diff --git a/examples/chain_client/16_MsgSubaccountTransfer.py b/examples/chain_client/16_MsgSubaccountTransfer.py index 6d02e5c2..7ec93b4c 100644 --- a/examples/chain_client/16_MsgSubaccountTransfer.py +++ b/examples/chain_client/16_MsgSubaccountTransfer.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -11,10 +9,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account diff --git a/examples/chain_client/17_MsgBatchUpdateOrders.py b/examples/chain_client/17_MsgBatchUpdateOrders.py index 7ec36bd0..797e0a6c 100644 --- a/examples/chain_client/17_MsgBatchUpdateOrders.py +++ b/examples/chain_client/17_MsgBatchUpdateOrders.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -11,10 +9,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account @@ -132,7 +130,7 @@ async def main() -> None: print(sim_res) return - sim_res_msg = ProtoMsgComposer.MsgResponses(sim_res, simulation=True) + sim_res_msg = composer.MsgResponses(sim_res, simulation=True) print("---Simulation Response---") print(sim_res_msg) diff --git a/examples/chain_client/18_MsgBid.py b/examples/chain_client/18_MsgBid.py index 35ab0256..e48ef1fa 100644 --- a/examples/chain_client/18_MsgBid.py +++ b/examples/chain_client/18_MsgBid.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -10,10 +8,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account diff --git a/examples/chain_client/19_MsgGrant.py b/examples/chain_client/19_MsgGrant.py index da9fbbb5..2b0bbb7b 100644 --- a/examples/chain_client/19_MsgGrant.py +++ b/examples/chain_client/19_MsgGrant.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -11,10 +9,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account diff --git a/examples/chain_client/1_MsgSend.py b/examples/chain_client/1_MsgSend.py index f3a48769..0a37afc1 100644 --- a/examples/chain_client/1_MsgSend.py +++ b/examples/chain_client/1_MsgSend.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -11,11 +9,11 @@ async def main() -> None: # select network: local, testnet, mainnet network = Network.testnet() - composer = ProtoMsgComposer(network=network.string()) # initialize grpc client # set custom cookie location (optional) - defaults to current dir client = AsyncClient(network, insecure=False, chain_cookie_location="/tmp/.chain_cookie") + composer = await client.composer() await client.sync_timeout_height() # load account diff --git a/examples/chain_client/20_MsgExec.py b/examples/chain_client/20_MsgExec.py index 8aafd5d4..231a939f 100644 --- a/examples/chain_client/20_MsgExec.py +++ b/examples/chain_client/20_MsgExec.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -11,10 +9,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account @@ -63,9 +61,9 @@ async def main() -> None: print(sim_res) return - sim_res_msg = ProtoMsgComposer.MsgResponses(sim_res, simulation=True) + sim_res_msg = composer.MsgResponses(sim_res, simulation=True) data=sim_res_msg[0] - unpacked_msg_res = ProtoMsgComposer.UnpackMsgExecResponse( + unpacked_msg_res = composer.UnpackMsgExecResponse( msg_type=msg0.__class__.__name__, data=data ) diff --git a/examples/chain_client/21_MsgRevoke.py b/examples/chain_client/21_MsgRevoke.py index b01d799c..e6d35bd2 100644 --- a/examples/chain_client/21_MsgRevoke.py +++ b/examples/chain_client/21_MsgRevoke.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -11,10 +9,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account diff --git a/examples/chain_client/22_MsgSendToEth.py b/examples/chain_client/22_MsgSendToEth.py index 91ecceb3..3b9abfc8 100644 --- a/examples/chain_client/22_MsgSendToEth.py +++ b/examples/chain_client/22_MsgSendToEth.py @@ -1,8 +1,6 @@ import asyncio -import logging import requests -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -11,10 +9,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account diff --git a/examples/chain_client/23_MsgRelayPriceFeedPrice.py b/examples/chain_client/23_MsgRelayPriceFeedPrice.py index 592e993e..1f9bfc53 100644 --- a/examples/chain_client/23_MsgRelayPriceFeedPrice.py +++ b/examples/chain_client/23_MsgRelayPriceFeedPrice.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -11,10 +9,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account diff --git a/examples/chain_client/24_MsgRewardsOptOut.py b/examples/chain_client/24_MsgRewardsOptOut.py index eb413004..34c48bc7 100644 --- a/examples/chain_client/24_MsgRewardsOptOut.py +++ b/examples/chain_client/24_MsgRewardsOptOut.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -11,10 +9,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account diff --git a/examples/chain_client/25_MsgDelegate.py b/examples/chain_client/25_MsgDelegate.py index fb78dba8..d040dfd2 100644 --- a/examples/chain_client/25_MsgDelegate.py +++ b/examples/chain_client/25_MsgDelegate.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -11,10 +9,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account diff --git a/examples/chain_client/26_MsgWithdrawDelegatorReward.py b/examples/chain_client/26_MsgWithdrawDelegatorReward.py index a1b955a7..c133f0ec 100644 --- a/examples/chain_client/26_MsgWithdrawDelegatorReward.py +++ b/examples/chain_client/26_MsgWithdrawDelegatorReward.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -11,10 +9,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account diff --git a/examples/chain_client/2_MsgDeposit.py b/examples/chain_client/2_MsgDeposit.py index bcb6f78e..a1684ddc 100644 --- a/examples/chain_client/2_MsgDeposit.py +++ b/examples/chain_client/2_MsgDeposit.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -11,10 +9,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account diff --git a/examples/chain_client/30_ExternalTransfer.py b/examples/chain_client/30_ExternalTransfer.py index 26defc09..1fcef982 100644 --- a/examples/chain_client/30_ExternalTransfer.py +++ b/examples/chain_client/30_ExternalTransfer.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -11,10 +9,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account diff --git a/examples/chain_client/31_MsgCreateBinaryOptionsLimitOrder.py b/examples/chain_client/31_MsgCreateBinaryOptionsLimitOrder.py index ab79d52a..0d33b93d 100644 --- a/examples/chain_client/31_MsgCreateBinaryOptionsLimitOrder.py +++ b/examples/chain_client/31_MsgCreateBinaryOptionsLimitOrder.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network, Denom @@ -10,10 +8,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account @@ -62,7 +60,7 @@ async def main() -> None: print(sim_res) return - sim_res_msg = ProtoMsgComposer.MsgResponses(sim_res, simulation=True) + sim_res_msg = composer.MsgResponses(sim_res, simulation=True) print("---Simulation Response---") print(sim_res_msg) diff --git a/examples/chain_client/32_MsgCreateBinaryOptionsMarketOrder.py b/examples/chain_client/32_MsgCreateBinaryOptionsMarketOrder.py index 31d9cf04..12b808c4 100644 --- a/examples/chain_client/32_MsgCreateBinaryOptionsMarketOrder.py +++ b/examples/chain_client/32_MsgCreateBinaryOptionsMarketOrder.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -11,10 +9,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account @@ -57,7 +55,7 @@ async def main() -> None: print(sim_res) return - sim_res_msg = ProtoMsgComposer.MsgResponses(sim_res, simulation=True) + sim_res_msg = composer.MsgResponses(sim_res, simulation=True) print("---Simulation Response---") print(sim_res_msg) diff --git a/examples/chain_client/33_MsgCancelBinaryOptionsOrder.py b/examples/chain_client/33_MsgCancelBinaryOptionsOrder.py index ca51d084..7a4d1197 100644 --- a/examples/chain_client/33_MsgCancelBinaryOptionsOrder.py +++ b/examples/chain_client/33_MsgCancelBinaryOptionsOrder.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -11,10 +9,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account @@ -53,7 +51,7 @@ async def main() -> None: print(sim_res) return - sim_res_msg = ProtoMsgComposer.MsgResponses(sim_res, simulation=True) + sim_res_msg = composer.MsgResponses(sim_res, simulation=True) print("---Simulation Response---") print(sim_res_msg) diff --git a/examples/chain_client/34_MsgAdminUpdateBinaryOptionsMarket.py b/examples/chain_client/34_MsgAdminUpdateBinaryOptionsMarket.py index 2f665919..5037c899 100644 --- a/examples/chain_client/34_MsgAdminUpdateBinaryOptionsMarket.py +++ b/examples/chain_client/34_MsgAdminUpdateBinaryOptionsMarket.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -11,10 +9,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account @@ -58,7 +56,7 @@ async def main() -> None: print(sim_res) return - sim_res_msg = ProtoMsgComposer.MsgResponses(sim_res, simulation=True) + sim_res_msg = composer.MsgResponses(sim_res, simulation=True) print("---Simulation Response---") print(sim_res_msg) diff --git a/examples/chain_client/35_MsgInstantBinaryOptionsMarketLaunch.py b/examples/chain_client/35_MsgInstantBinaryOptionsMarketLaunch.py index 3b12ab87..e20e54b9 100644 --- a/examples/chain_client/35_MsgInstantBinaryOptionsMarketLaunch.py +++ b/examples/chain_client/35_MsgInstantBinaryOptionsMarketLaunch.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -11,10 +9,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account @@ -60,7 +58,7 @@ async def main() -> None: print(sim_res) return - sim_res_msg = ProtoMsgComposer.MsgResponses(sim_res, simulation=True) + sim_res_msg = composer.MsgResponses(sim_res, simulation=True) print("---Simulation Response---") print(sim_res_msg) diff --git a/examples/chain_client/36_MsgRelayProviderPrices.py b/examples/chain_client/36_MsgRelayProviderPrices.py index a579fb6a..bd88d650 100644 --- a/examples/chain_client/36_MsgRelayProviderPrices.py +++ b/examples/chain_client/36_MsgRelayProviderPrices.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -11,10 +9,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account @@ -53,7 +51,7 @@ async def main() -> None: print(sim_res) return - sim_res_msg = ProtoMsgComposer.MsgResponses(sim_res, simulation=True) + sim_res_msg = composer.MsgResponses(sim_res, simulation=True) print("---Simulation Response---") print(sim_res_msg) diff --git a/examples/chain_client/3_MsgCreateSpotLimitOrder.py b/examples/chain_client/3_MsgCreateSpotLimitOrder.py index 04cb0ffa..5501b01b 100644 --- a/examples/chain_client/3_MsgCreateSpotLimitOrder.py +++ b/examples/chain_client/3_MsgCreateSpotLimitOrder.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -11,10 +9,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account @@ -58,7 +56,7 @@ async def main() -> None: print(sim_res) return - sim_res_msg = ProtoMsgComposer.MsgResponses(sim_res, simulation=True) + sim_res_msg = composer.MsgResponses(sim_res, simulation=True) print("---Simulation Response---") print(sim_res_msg) diff --git a/examples/chain_client/40_MsgExecuteContract.py b/examples/chain_client/40_MsgExecuteContract.py index 4661f4b4..d3ab1dda 100644 --- a/examples/chain_client/40_MsgExecuteContract.py +++ b/examples/chain_client/40_MsgExecuteContract.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -11,11 +9,11 @@ async def main() -> None: # select network: local, testnet, mainnet network = Network.testnet() - composer = ProtoMsgComposer(network=network.string()) # initialize grpc client # set custom cookie location (optional) - defaults to current dir client = AsyncClient(network, insecure=False) + composer = await client.composer() await client.sync_timeout_height() # load account diff --git a/examples/chain_client/41_MsgCreateInsuranceFund.py b/examples/chain_client/41_MsgCreateInsuranceFund.py index 255419b0..1a511f05 100644 --- a/examples/chain_client/41_MsgCreateInsuranceFund.py +++ b/examples/chain_client/41_MsgCreateInsuranceFund.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -10,11 +8,11 @@ async def main() -> None: # select network: local, testnet, mainnet network = Network.testnet() - composer = ProtoMsgComposer(network=network.string()) # initialize grpc client # set custom cookie location (optional) - defaults to current dir client = AsyncClient(network, insecure=False) + composer = await client.composer() await client.sync_timeout_height() # load account diff --git a/examples/chain_client/42_MsgUnderwrite.py b/examples/chain_client/42_MsgUnderwrite.py index 49aa7313..d7d04fb3 100644 --- a/examples/chain_client/42_MsgUnderwrite.py +++ b/examples/chain_client/42_MsgUnderwrite.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -10,11 +8,11 @@ async def main() -> None: # select network: local, testnet, mainnet network = Network.testnet() - composer = ProtoMsgComposer(network=network.string()) # initialize grpc client # set custom cookie location (optional) - defaults to current dir client = AsyncClient(network, insecure=False) + composer = await client.composer() await client.sync_timeout_height() # load account diff --git a/examples/chain_client/43_MsgRequestRedemption.py b/examples/chain_client/43_MsgRequestRedemption.py index a37a774d..640a42d2 100644 --- a/examples/chain_client/43_MsgRequestRedemption.py +++ b/examples/chain_client/43_MsgRequestRedemption.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -10,11 +8,11 @@ async def main() -> None: # select network: local, testnet, mainnet network = Network.testnet() - composer = ProtoMsgComposer(network=network.string()) # initialize grpc client # set custom cookie location (optional) - defaults to current dir client = AsyncClient(network, insecure=False) + composer = await client.composer() await client.sync_timeout_height() # load account diff --git a/examples/chain_client/4_MsgCreateSpotMarketOrder.py b/examples/chain_client/4_MsgCreateSpotMarketOrder.py index d851517c..0f234d0b 100644 --- a/examples/chain_client/4_MsgCreateSpotMarketOrder.py +++ b/examples/chain_client/4_MsgCreateSpotMarketOrder.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -11,10 +9,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account @@ -57,7 +55,7 @@ async def main() -> None: print(sim_res) return - sim_res_msg = ProtoMsgComposer.MsgResponses(sim_res, simulation=True) + sim_res_msg = composer.MsgResponses(sim_res, simulation=True) print("---Simulation Response---") print(sim_res_msg) diff --git a/examples/chain_client/5_MsgCancelSpotOrder.py b/examples/chain_client/5_MsgCancelSpotOrder.py index a347a54b..ced2467f 100644 --- a/examples/chain_client/5_MsgCancelSpotOrder.py +++ b/examples/chain_client/5_MsgCancelSpotOrder.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -11,10 +9,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account diff --git a/examples/chain_client/6_MsgCreateDerivativeLimitOrder.py b/examples/chain_client/6_MsgCreateDerivativeLimitOrder.py index f53667b3..8753d2d9 100644 --- a/examples/chain_client/6_MsgCreateDerivativeLimitOrder.py +++ b/examples/chain_client/6_MsgCreateDerivativeLimitOrder.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -11,10 +9,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account @@ -25,7 +23,7 @@ async def main() -> None: subaccount_id = address.get_subaccount_id(index=0) # prepare trade info - market_id = "0x90e662193fa29a3a7e6c07be4407c94833e762d9ee82136a2cc712d6b87d7de3" + market_id = "0x141e3c92ed55107067ceb60ee412b86256cedef67b1227d6367b4cdf30c55a74" fee_recipient = "inj1hkhdaj2a2clmq5jq6mspsggqs32vynpk228q3r" # prepare tx msg @@ -59,7 +57,7 @@ async def main() -> None: print(sim_res) return - sim_res_msg = ProtoMsgComposer.MsgResponses(sim_res, simulation=True) + sim_res_msg = composer.MsgResponses(sim_res, simulation=True) print("---Simulation Response---") print(sim_res_msg) diff --git a/examples/chain_client/7_MsgCreateDerivativeMarketOrder.py b/examples/chain_client/7_MsgCreateDerivativeMarketOrder.py index a8d456d1..edcfb6c4 100644 --- a/examples/chain_client/7_MsgCreateDerivativeMarketOrder.py +++ b/examples/chain_client/7_MsgCreateDerivativeMarketOrder.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -11,10 +9,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account @@ -58,7 +56,7 @@ async def main() -> None: print(sim_res) return - sim_res_msg = ProtoMsgComposer.MsgResponses(sim_res, simulation=True) + sim_res_msg = composer.MsgResponses(sim_res, simulation=True) print("---Simulation Response---") print(sim_res_msg) diff --git a/examples/chain_client/8_MsgCancelDerivativeOrder.py b/examples/chain_client/8_MsgCancelDerivativeOrder.py index 7abbf16a..66afe95d 100644 --- a/examples/chain_client/8_MsgCancelDerivativeOrder.py +++ b/examples/chain_client/8_MsgCancelDerivativeOrder.py @@ -1,7 +1,5 @@ import asyncio -import logging -from pyinjective.composer import Composer as ProtoMsgComposer from pyinjective.async_client import AsyncClient from pyinjective.transaction import Transaction from pyinjective.constant import Network @@ -11,10 +9,10 @@ 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) + composer = await client.composer() await client.sync_timeout_height() # load account diff --git a/examples/exchange_client/spot_exchange_rpc/14_Orderbooks.py b/examples/exchange_client/spot_exchange_rpc/14_Orderbooks.py index 0e792d5a..59d23047 100644 --- a/examples/exchange_client/spot_exchange_rpc/14_Orderbooks.py +++ b/examples/exchange_client/spot_exchange_rpc/14_Orderbooks.py @@ -11,7 +11,7 @@ async def main() -> None: "0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe", "0x7a57e705bb4e09c88aecfc295569481dbf2fe1d5efe364651fbe72385938e9b0" ] - orderbooks = await client.get_spot_orderbooks(market_ids=market_ids) + orderbooks = await client.get_spot_orderbooksV2(market_ids=market_ids) print(orderbooks) if __name__ == '__main__': diff --git a/examples/exchange_client/spot_exchange_rpc/16_OrderbooksV2.py b/examples/exchange_client/spot_exchange_rpc/16_OrderbooksV2.py deleted file mode 100644 index 6a28009e..00000000 --- a/examples/exchange_client/spot_exchange_rpc/16_OrderbooksV2.py +++ /dev/null @@ -1,19 +0,0 @@ -import asyncio -import logging - -from pyinjective.async_client import AsyncClient -from pyinjective.constant import Network - -async def main() -> None: - # select network: local, testnet, mainnet - network = Network.testnet() - client = AsyncClient(network, insecure=False) - market_ids = [ - "0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe", - "0x7a57e705bb4e09c88aecfc295569481dbf2fe1d5efe364651fbe72385938e9b0" - ] - orderbooks = await client.get_spot_orderbooksV2(market_ids=market_ids) - print(orderbooks) - -if __name__ == '__main__': - asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/exchange_client/spot_exchange_rpc/4_Orderbook.py b/examples/exchange_client/spot_exchange_rpc/4_Orderbook.py index 44522fbc..19b58933 100644 --- a/examples/exchange_client/spot_exchange_rpc/4_Orderbook.py +++ b/examples/exchange_client/spot_exchange_rpc/4_Orderbook.py @@ -8,7 +8,7 @@ async def main() -> None: network = Network.testnet() client = AsyncClient(network, insecure=False) market_id = "0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe" - orderbook = await client.get_spot_orderbook(market_id=market_id) + orderbook = await client.get_spot_orderbookV2(market_id=market_id) print(orderbook) if __name__ == '__main__': diff --git a/pyinjective/async_client.py b/pyinjective/async_client.py index 889cd038..253bb4d9 100644 --- a/pyinjective/async_client.py +++ b/pyinjective/async_client.py @@ -1,13 +1,20 @@ -import os +import asyncio import time +from copy import deepcopy + import grpc import aiocron -import logging import datetime +from decimal import Decimal from http.cookies import SimpleCookie -from typing import List, Optional, Tuple, Union +from typing import Dict, List, Optional, Tuple, Union + +from pyinjective.composer import Composer -from .exceptions import NotFoundError, EmptyMsgError +from . import constant +from .core.market import BinaryOptionMarket, DerivativeMarket, SpotMarket +from .core.token import Token +from .exceptions import NotFoundError from .proto.cosmos.base.abci.v1beta1 import abci_pb2 as abci_type @@ -179,6 +186,40 @@ def __init__( start=True, ) + self._tokens_and_markets_initialization_lock = asyncio.Lock() + self._tokens: Optional[Dict[str, Token]] = None + self._spot_markets: Optional[Dict[str, SpotMarket]] = None + self._derivative_markets: Optional[Dict[str, DerivativeMarket]] = None + self._binary_option_markets: Optional[Dict[str, BinaryOptionMarket]] = None + + async def all_tokens(self) -> Dict[str, Token]: + if self._tokens is None: + async with self._tokens_and_markets_initialization_lock: + if self._tokens is None: + await self._initialize_tokens_and_markets() + return deepcopy(self._tokens) + + async def all_spot_markets(self) -> Dict[str, SpotMarket]: + if self._spot_markets is None: + async with self._tokens_and_markets_initialization_lock: + if self._spot_markets is None: + await self._initialize_tokens_and_markets() + return deepcopy(self._spot_markets) + + async def all_derivative_markets(self) -> Dict[str, DerivativeMarket]: + if self._derivative_markets is None: + async with self._tokens_and_markets_initialization_lock: + if self._derivative_markets is None: + await self._initialize_tokens_and_markets() + return deepcopy(self._derivative_markets) + + async def all_binary_option_markets(self) -> Dict[str, BinaryOptionMarket]: + if self._binary_option_markets is None: + async with self._tokens_and_markets_initialization_lock: + if self._binary_option_markets is None: + await self._initialize_tokens_and_markets() + return deepcopy(self._binary_option_markets) + def get_sequence(self): current_seq = self.sequence self.sequence += 1 @@ -634,14 +675,6 @@ async def stream_spot_markets(self, **kwargs): metadata = await self.load_cookie(type="exchange") return self.stubSpotExchange.StreamMarkets.__call__(req, metadata=metadata) - async def get_spot_orderbook(self, market_id: str): - req = spot_exchange_rpc_pb.OrderbookRequest(market_id=market_id) - return await self.stubSpotExchange.Orderbook(req) - - async def get_spot_orderbooks(self, market_ids: List): - req = spot_exchange_rpc_pb.OrderbooksRequest(market_ids=market_ids) - return await self.stubSpotExchange.Orderbooks(req) - async def get_spot_orderbookV2(self, market_id: str): req = spot_exchange_rpc_pb.OrderbookV2Request(market_id=market_id) return await self.stubSpotExchange.OrderbookV2(req) @@ -984,7 +1017,7 @@ async def get_funding_rates(self, market_id: str, **kwargs): skip=kwargs.get("skip"), limit=kwargs.get("limit"), end_time=kwargs.get("end_time"), -) + ) return await self.stubDerivativeExchange.FundingRates(req) async def get_binary_options_markets(self, **kwargs): @@ -1014,3 +1047,140 @@ async def stream_account_portfolio(self, account_address: str, **kwargs): ) metadata = await self.load_cookie(type="exchange") return self.stubPortfolio.StreamAccountPortfolio.__call__(req, metadata=metadata) + + async def composer(self): + return Composer( + network=self.network.string(), + spot_markets=await self.all_spot_markets(), + derivative_markets=await self.all_derivative_markets(), + binary_option_markets=await self.all_binary_option_markets(), + tokens=await self.all_tokens(), + ) + + async def _initialize_tokens_and_markets(self): + spot_markets = dict() + derivative_markets = dict() + binary_option_markets = dict() + tokens = dict() + tokens_by_denom = dict() + markets_info = (await self.get_spot_markets()).markets + + for market_info in markets_info: + base_token_symbol, quote_token_symbol = market_info.ticker.split(constant.TICKER_TOKENS_SEPARATOR) + base_token = self._token_representation( + symbol=base_token_symbol, + token_meta=market_info.base_token_meta, + denom=market_info.base_denom, + all_tokens=tokens, + ) + if base_token.denom not in tokens_by_denom: + tokens_by_denom[base_token.denom] = base_token + + quote_token = self._token_representation( + symbol=quote_token_symbol, + token_meta=market_info.quote_token_meta, + denom=market_info.quote_denom, + all_tokens=tokens, + ) + if quote_token.denom not in tokens_by_denom: + tokens_by_denom[quote_token.denom] = quote_token + + market = SpotMarket( + id=market_info.market_id, + status=market_info.market_status, + ticker=market_info.ticker, + base_token=base_token, + quote_token=quote_token, + maker_fee_rate=Decimal(market_info.maker_fee_rate), + taker_fee_rate=Decimal(market_info.taker_fee_rate), + service_provider_fee=Decimal(market_info.service_provider_fee), + min_price_tick_size=Decimal(market_info.min_price_tick_size), + min_quantity_tick_size=Decimal(market_info.min_quantity_tick_size), + ) + + spot_markets[market.id] = market + + markets_info = (await self.get_derivative_markets()).markets + for market_info in markets_info: + quote_token_symbol = market_info.quote_token_meta.symbol + + quote_token = self._token_representation( + symbol=quote_token_symbol, + token_meta=market_info.quote_token_meta, + denom=market_info.quote_denom, + all_tokens=tokens, + ) + if quote_token.denom not in tokens_by_denom: + tokens_by_denom[quote_token.denom] = quote_token + + market = DerivativeMarket( + id=market_info.market_id, + status=market_info.market_status, + ticker=market_info.ticker, + oracle_base=market_info.oracle_base, + oracle_quote=market_info.oracle_quote, + oracle_type=market_info.oracle_type, + oracle_scale_factor=market_info.oracle_scale_factor, + initial_margin_ratio=Decimal(market_info.initial_margin_ratio), + maintenance_margin_ratio=Decimal(market_info.maintenance_margin_ratio), + quote_token=quote_token, + maker_fee_rate=Decimal(market_info.maker_fee_rate), + taker_fee_rate = Decimal(market_info.taker_fee_rate), + service_provider_fee = Decimal(market_info.service_provider_fee), + min_price_tick_size = Decimal(market_info.min_price_tick_size), + min_quantity_tick_size = Decimal(market_info.min_quantity_tick_size), + ) + + derivative_markets[market.id] = market + + markets_info = (await self.get_binary_options_markets()).markets + for market_info in markets_info: + quote_token = tokens_by_denom.get(market_info.quote_denom, None) + + market = BinaryOptionMarket( + id=market_info.market_id, + status=market_info.market_status, + ticker=market_info.ticker, + oracle_symbol=market_info.oracle_symbol, + oracle_provider=market_info.oracle_provider, + oracle_type=market_info.oracle_type, + oracle_scale_factor=market_info.oracle_scale_factor, + expiration_timestamp=market_info.expiration_timestamp, + settlement_timestamp=market_info.settlement_timestamp, + quote_token=quote_token, + maker_fee_rate=Decimal(market_info.maker_fee_rate), + taker_fee_rate=Decimal(market_info.taker_fee_rate), + service_provider_fee=Decimal(market_info.service_provider_fee), + min_price_tick_size=Decimal(market_info.min_price_tick_size), + min_quantity_tick_size=Decimal(market_info.min_quantity_tick_size), + ) + + binary_option_markets[market.id] = market + + self._tokens = tokens + self._spot_markets = spot_markets + self._derivative_markets = derivative_markets + self._binary_option_markets = binary_option_markets + + def _token_representation(self, symbol: str, token_meta, denom: str, all_tokens: Dict[str, Token]) -> Token: + token = Token( + name=token_meta.name, + symbol=symbol, + denom=denom, + address=token_meta.address, + decimals=token_meta.decimals, + logo=token_meta.logo, + updated=token_meta.updated_at, + ) + + existing_token = all_tokens.get(token.symbol, None) + if existing_token is None: + all_tokens[token.symbol] = token + existing_token = token + elif existing_token.denom != denom: + existing_token = all_tokens.get(token.name, None) + if existing_token is None: + all_tokens[token.name] = token + existing_token = token + + return existing_token diff --git a/pyinjective/client.py b/pyinjective/client.py index 662d9840..10b82329 100644 --- a/pyinjective/client.py +++ b/pyinjective/client.py @@ -6,7 +6,7 @@ from http.cookies import SimpleCookie from typing import List, Optional, Tuple, Union -from .exceptions import NotFoundError, EmptyMsgError +from .exceptions import NotFoundError from .proto.cosmos.base.abci.v1beta1 import abci_pb2 as abci_type diff --git a/pyinjective/composer.py b/pyinjective/composer.py index 5db90cc9..2b3dd728 100644 --- a/pyinjective/composer.py +++ b/pyinjective/composer.py @@ -1,9 +1,11 @@ +from configparser import ConfigParser +from decimal import Decimal from time import time -import json -import logging from google.protobuf import any_pb2, timestamp_pb2, json_format +from .core.market import BinaryOptionMarket, DerivativeMarket, SpotMarket +from .core.token import Token from .proto.cosmos.authz.v1beta1 import authz_pb2 as cosmos_authz_pb from .proto.cosmos.authz.v1beta1 import tx_pb2 as cosmos_authz_tx_pb @@ -32,16 +34,51 @@ from .proto.cosmwasm.wasm.v1 import tx_pb2 as wasm_tx_pb -from .constant import Denom -from .utils.utils import * -from typing import List +from .constant import ADDITIONAL_CHAIN_FORMAT_DECIMALS, INJ_DENOM +from typing import Dict, List, Optional -from pyinjective.utils.logger import LoggerProvider +from pyinjective import constant class Composer: - def __init__(self, network: str): + def __init__( + self, + network: str, + spot_markets: Optional[Dict[str, SpotMarket]] = None, + derivative_markets: Optional[Dict[str, DerivativeMarket]] = None, + binary_option_markets: Optional[Dict[str, BinaryOptionMarket]] = None, + tokens: Optional[Dict[str, Token]] = None): + """ Composer is used to create the requests to send to the nodes using the Client + + :param network: the name of the network to use (mainnet, testnet, devnet) + :type network: str + + :param spot_markets: a dictionary containing all spot markets + :type spot_markets: Dictionary + + :param derivative_markets: a dictionary containing all derivative markets + :type derivative_markets: Dictionary + + :param binary_option_markets: a dictionary containing all derivative markets + :type binary_option_markets: Dictionary + + :param tokens: a dictionary with all the possible tokens + :type tokens: Dictionary + + + """ self.network = network + self.spot_markets = dict() + self.derivative_markets = dict() + self.binary_option_markets = dict() + self.tokens = dict() + if spot_markets is None or derivative_markets is None or binary_option_markets is None or tokens is None: + self._initialize_markets_and_tokens_from_files() + else: + self.spot_markets = spot_markets + self.derivative_markets = derivative_markets + self.binary_option_markets = binary_option_markets + self.tokens = tokens def Coin(self, amount: int, denom: str): return cosmos_dot_base_dot_v1beta1_dot_coin__pb2.Coin(amount=str(amount), denom=denom) @@ -91,15 +128,12 @@ def SpotOrder( quantity: float, **kwargs, ): - # load denom metadata - denom = Denom.load_market(self.network, market_id) - LoggerProvider().logger_for_class(logging_class=self.__class__).info( - f"Loaded market metadata for: {denom.description}") + market = self.spot_markets[market_id] # prepare values - quantity = spot_quantity_to_backend(quantity, denom) - price = spot_price_to_backend(price, denom) - trigger_price = spot_price_to_backend(0, denom) + quantity = market.quantity_to_chain_format(human_readable_value=Decimal(str(quantity))) + price = market.price_to_chain_format(human_readable_value=Decimal(str(price))) + trigger_price = market.price_to_chain_format(human_readable_value=Decimal(0)) if kwargs.get("is_buy") and not kwargs.get("is_po"): order_type = injective_dot_exchange_dot_v1beta1_dot_exchange__pb2.OrderType.BUY @@ -118,11 +152,11 @@ def SpotOrder( order_info=injective_dot_exchange_dot_v1beta1_dot_exchange__pb2.OrderInfo( subaccount_id=subaccount_id, fee_recipient=fee_recipient, - price=str(price), - quantity=str(quantity), + price=str(int(price)), + quantity=str(int(quantity)), ), order_type=order_type, - trigger_price=str(trigger_price), + trigger_price=str(int(trigger_price)), ) def DerivativeOrder( @@ -135,26 +169,21 @@ def DerivativeOrder( trigger_price: float = 0, **kwargs, ): - # load denom metadata - denom = Denom.load_market(self.network, market_id) - LoggerProvider().logger_for_class(logging_class=self.__class__).info( - f"Loaded market metadata for: {denom.description}") - - if kwargs.get("is_reduce_only") is None: - margin = derivative_margin_to_backend( - price, quantity, kwargs.get("leverage"), denom - ) - elif kwargs.get("is_reduce_only", True): + market = self.derivative_markets[market_id] + + if kwargs.get("is_reduce_only", False): margin = 0 else: - margin = derivative_margin_to_backend( - price, quantity, kwargs.get("leverage"), denom + margin = market.calculate_margin_in_chain_format( + human_readable_quantity=Decimal(str(quantity)), + human_readable_price=Decimal(str(price)), + leverage=Decimal(str(kwargs["leverage"])), ) # prepare values - price = derivative_price_to_backend(price, denom) - trigger_price = derivative_price_to_backend(trigger_price, denom) - quantity = derivative_quantity_to_backend(quantity, denom) + quantity = market.quantity_to_chain_format(human_readable_value=Decimal(str(quantity))) + price = market.price_to_chain_format(human_readable_value=Decimal(str(price))) + trigger_price = market.price_to_chain_format(human_readable_value=Decimal(str(trigger_price))) if kwargs.get("is_buy") and not kwargs.get("is_po"): order_type = injective_dot_exchange_dot_v1beta1_dot_exchange__pb2.OrderType.BUY @@ -185,12 +214,12 @@ def DerivativeOrder( order_info=injective_dot_exchange_dot_v1beta1_dot_exchange__pb2.OrderInfo( subaccount_id=subaccount_id, fee_recipient=fee_recipient, - price=str(price), - quantity=str(quantity), + price=str(int(price)), + quantity=str(int(quantity)), ), - margin=str(margin), + margin=str(int(margin)), order_type=order_type, - trigger_price=str(trigger_price), + trigger_price=str(int(trigger_price)), ) def BinaryOptionsOrder( @@ -203,30 +232,23 @@ def BinaryOptionsOrder( **kwargs, ): - if "denom" in kwargs: - denom = kwargs.get("denom") - else: - denom = Denom.load_market(self.network, market_id) - - # load denom metadata - LoggerProvider().logger_for_class(logging_class=self.__class__).info( - f"Loaded market metadata for: {denom.description}") - - if kwargs.get("is_reduce_only") is None and kwargs.get("is_buy"): - margin = binary_options_buy_margin_to_backend(price, quantity, denom) - elif kwargs.get("is_reduce_only") is None and not kwargs.get("is_buy"): - margin = binary_options_sell_margin_to_backend(price, quantity, denom) - elif kwargs.get("is_reduce_only") is False and kwargs.get("is_buy"): - margin = binary_options_buy_margin_to_backend(price, quantity, denom) - elif kwargs.get("is_reduce_only") is False and not kwargs.get("is_buy"): - margin = binary_options_sell_margin_to_backend(price, quantity, denom) - elif kwargs.get("is_reduce_only", True): + market = self.binary_option_markets[market_id] + denom = kwargs.get("denom", None) + + if kwargs.get("is_reduce_only", False): margin = 0 + else: + margin = market.calculate_margin_in_chain_format( + human_readable_quantity=Decimal(str(quantity)), + human_readable_price=Decimal(str(price)), + is_buy=kwargs["is_buy"], + special_denom=denom, + ) # prepare values - price = binary_options_price_to_backend(price, denom) - trigger_price = binary_options_price_to_backend(0, denom) - quantity = binary_options_quantity_to_backend(quantity, denom) + price = market.price_to_chain_format(human_readable_value=Decimal(str(price)), special_denom=denom) + trigger_price = market.price_to_chain_format(human_readable_value=Decimal(str(0)), special_denom=denom) + quantity = market.quantity_to_chain_format(human_readable_value=Decimal(str(quantity)), special_denom=denom) if kwargs.get("is_buy") and not kwargs.get("is_po"): order_type = injective_dot_exchange_dot_v1beta1_dot_exchange__pb2.OrderType.BUY @@ -245,25 +267,22 @@ def BinaryOptionsOrder( order_info=injective_dot_exchange_dot_v1beta1_dot_exchange__pb2.OrderInfo( subaccount_id=subaccount_id, fee_recipient=fee_recipient, - price=str(price), - quantity=str(quantity), + price=str(int(price)), + quantity=str(int(quantity)), ), - margin=str(margin), + margin=str(int(margin)), order_type=order_type, - trigger_price=str(trigger_price), + trigger_price=str(int(trigger_price)), ) def MsgSend(self, from_address: str, to_address: str, amount: float, denom: str): - peggy_denom, decimals = Denom.load_peggy_denom(self.network, denom) - be_amount = amount_to_backend(amount, decimals) - LoggerProvider().logger_for_class(logging_class=self.__class__).info( - f"Loaded send symbol {denom} ({peggy_denom}) with decimals = {decimals}" - ) + token = self.tokens[denom] + be_amount = token.chain_formatted_value(human_readable_value=Decimal(str(amount))) return cosmos_bank_tx_pb.MsgSend( from_address=from_address, to_address=to_address, - amount=[self.Coin(amount=be_amount, denom=peggy_denom)], + amount=[self.Coin(amount=int(be_amount), denom=token.denom)], ) def MsgExecuteContract(self, sender: str, contract: str, msg: str, **kwargs): @@ -275,16 +294,13 @@ def MsgExecuteContract(self, sender: str, contract: str, msg: str, **kwargs): ) def MsgDeposit(self, sender: str, subaccount_id: str, amount: float, denom: str): - peggy_denom, decimals = Denom.load_peggy_denom(self.network, denom) - be_amount = amount_to_backend(amount, decimals) - LoggerProvider().logger_for_class(logging_class=self.__class__).info( - f"Loaded deposit symbol {denom} ({peggy_denom}) with decimals = {decimals}" - ) + token = self.tokens[denom] + be_amount = token.chain_formatted_value(human_readable_value=Decimal(str(amount))) return injective_exchange_tx_pb.MsgDeposit( sender=sender, subaccount_id=subaccount_id, - amount=self.Coin(amount=be_amount, denom=peggy_denom), + amount=self.Coin(amount=int(be_amount), denom=token.denom), ) def MsgCreateSpotLimitOrder( @@ -607,14 +623,15 @@ def MsgIncreasePositionMargin( market_id: str, amount: float, ): - denom = Denom.load_market(self.network, market_id) - additional_margin = derivative_additional_margin_to_backend(amount, denom) + market = self.derivative_markets[market_id] + + additional_margin = market.margin_to_chain_format(human_readable_value=Decimal(str(amount))) return injective_exchange_tx_pb.MsgIncreasePositionMargin( sender=sender, source_subaccount_id=source_subaccount_id, destination_subaccount_id=destination_subaccount_id, market_id=market_id, - amount=str(additional_margin), + amount=str(int(additional_margin)), ) def MsgSubaccountTransfer( @@ -634,16 +651,13 @@ def MsgSubaccountTransfer( ) def MsgWithdraw(self, sender: str, subaccount_id: str, amount: float, denom: str): - peggy_denom, decimals = Denom.load_peggy_denom(self.network, denom) - be_amount = amount_to_backend(amount, decimals) - LoggerProvider().logger_for_class(logging_class=self.__class__).info( - f"Loaded withdrawal symbol {denom} ({peggy_denom}) with decimals = {decimals}" - ) + token = self.tokens[denom] + be_amount = token.chain_formatted_value(human_readable_value=Decimal(str(amount))) return injective_exchange_tx_pb.MsgWithdraw( sender=sender, subaccount_id=subaccount_id, - amount=self.Coin(amount=be_amount, denom=peggy_denom), + amount=self.Coin(amount=int(be_amount), denom=token.denom), ) def MsgExternalTransfer( @@ -654,27 +668,24 @@ def MsgExternalTransfer( amount: int, denom: str, ): - peggy_denom, decimals = Denom.load_peggy_denom(self.network, denom) - be_amount = amount_to_backend(amount, decimals) - LoggerProvider().logger_for_class(logging_class=self.__class__).info( - f"Loaded send symbol {denom} ({peggy_denom}) with decimals = {decimals}" - ) + token = self.tokens[denom] + be_amount = token.chain_formatted_value(human_readable_value=Decimal(str(amount))) return injective_exchange_tx_pb.MsgExternalTransfer( sender=sender, source_subaccount_id=source_subaccount_id, destination_subaccount_id=destination_subaccount_id, - amount=self.Coin(amount=be_amount, denom=peggy_denom), + amount=self.Coin(amount=int(be_amount), denom=token.denom), ) def MsgBid(self, sender: str, bid_amount: float, round: float): - be_amount = amount_to_backend(bid_amount, 18) + be_amount = Decimal(str(bid_amount)) * Decimal(f"1e{ADDITIONAL_CHAIN_FORMAT_DECIMALS}") return injective_auction_tx_pb.MsgBid( sender=sender, round=round, - bid_amount=self.Coin(amount=be_amount, denom="inj"), + bid_amount=self.Coin(amount=int(be_amount), denom=INJ_DENOM), ) def MsgGrantGeneric( @@ -787,31 +798,27 @@ def MsgRelayPriceFeedPrice( def MsgSendToEth( self, denom: str, sender: str, eth_dest: str, amount: float, bridge_fee: float ): - - peggy_denom, decimals = Denom.load_peggy_denom(self.network, denom) - be_amount = amount_to_backend(amount, decimals) - be_bridge_fee = amount_to_backend(bridge_fee, decimals) - LoggerProvider().logger_for_class(logging_class=self.__class__).info( - f"Loaded withdrawal symbol {denom} ({peggy_denom}) with decimals = {decimals}" - ) + token = self.tokens[denom] + be_amount = token.chain_formatted_value(human_readable_value=Decimal(str(amount))) + be_bridge_fee = token.chain_formatted_value(human_readable_value=Decimal(str(bridge_fee))) return injective_peggy_tx_pb.MsgSendToEth( sender=sender, eth_dest=eth_dest, - amount=self.Coin(amount=be_amount, denom=peggy_denom), - bridge_fee=self.Coin(amount=be_bridge_fee, denom=peggy_denom), + amount=self.Coin(amount=int(be_amount), denom=token.denom), + bridge_fee=self.Coin(amount=int(be_bridge_fee), denom=token.denom), ) def MsgDelegate( self, delegator_address: str, validator_address: str, amount: float ): - be_amount = amount_to_backend(amount, 18) + be_amount = Decimal(str(amount)) * Decimal(f"1e{ADDITIONAL_CHAIN_FORMAT_DECIMALS}") return cosmos_staking_tx_pb.MsgDelegate( delegator_address=delegator_address, validator_address=validator_address, - amount=self.Coin(amount=be_amount, denom="inj"), + amount=self.Coin(amount=int(be_amount), denom=INJ_DENOM), ) def MsgCreateInsuranceFund( @@ -825,15 +832,18 @@ def MsgCreateInsuranceFund( expiry: int, initial_deposit: int ): - peggy_denom, decimals = Denom.load_peggy_denom(self.network, quote_denom) - be_amount = amount_to_backend(initial_deposit, decimals) - LoggerProvider().logger_for_class(logging_class=self.__class__).info( - f"Loaded send symbol {quote_denom} ({peggy_denom}) with decimals = {decimals}" - ) + token = self.tokens[quote_denom] + be_amount = token.chain_formatted_value(human_readable_value=Decimal(str(initial_deposit))) return injective_insurance_tx_pb.MsgCreateInsuranceFund( - sender=sender, ticker=ticker, quote_denom=peggy_denom, oracle_base=oracle_base, oracle_quote=oracle_quote, - oracle_type=oracle_type, expiry=expiry, initial_deposit=self.Coin(amount=be_amount, denom=peggy_denom), + sender=sender, + ticker=ticker, + quote_denom=token.denom, + oracle_base=oracle_base, + oracle_quote=oracle_quote, + oracle_type=oracle_type, + expiry=expiry, + initial_deposit=self.Coin(amount=int(be_amount), denom=token.denom), ) def MsgUnderwrite( @@ -843,14 +853,11 @@ def MsgUnderwrite( quote_denom: str, amount: int, ): - peggy_denom, decimals = Denom.load_peggy_denom(self.network, quote_denom) - be_amount = amount_to_backend(amount, decimals) - LoggerProvider().logger_for_class(logging_class=self.__class__).info( - f"Loaded send symbol {quote_denom} ({peggy_denom}) with decimals = {decimals}" - ) + token = self.tokens[quote_denom] + be_amount = token.chain_formatted_value(human_readable_value=Decimal(str(amount))) return injective_insurance_tx_pb.MsgUnderwrite( - sender=sender, market_id=market_id, deposit=self.Coin(amount=be_amount, denom=peggy_denom), + sender=sender, market_id=market_id, deposit=self.Coin(amount=int(be_amount), denom=token.denom), ) def MsgRequestRedemption( @@ -999,3 +1006,169 @@ def UnpackTransactionMessages(transaction): msgs.append(json_format.Parse(msg_as_string_dict, header_map[msg["type"]]())) return msgs + + def _initialize_markets_and_tokens_from_files(self): + config: ConfigParser = constant.CONFIGS[self.network] + spot_markets = dict() + derivative_markets = dict() + tokens = dict() + + for section_name, configuration_section in config.items(): + if section_name.startswith("0x"): + description = configuration_section["description"] + + quote_token = Token( + name="", + symbol="", + denom="", + address="", + decimals=int(configuration_section["quote"]), + logo="", + updated=-1, + ) + + if "Spot" in description: + base_token = Token( + name="", + symbol="", + denom="", + address="", + decimals=int(configuration_section["base"]), + logo="", + updated=-1, + ) + + market = SpotMarket( + id=section_name, + status="", + ticker=description, + base_token=base_token, + quote_token=quote_token, + maker_fee_rate=None, + taker_fee_rate=None, + service_provider_fee=None, + min_price_tick_size=Decimal(str(configuration_section["min_price_tick_size"])), + min_quantity_tick_size=Decimal(str(configuration_section["min_quantity_tick_size"])) + ) + spot_markets[market.id] = market + else: + market = DerivativeMarket( + id=section_name, + status="", + ticker=description, + oracle_base="", + oracle_quote="", + oracle_type="", + oracle_scale_factor=1, + initial_margin_ratio=None, + maintenance_margin_ratio=None, + quote_token=quote_token, + maker_fee_rate=None, + taker_fee_rate=None, + service_provider_fee=None, + min_price_tick_size=Decimal(str(configuration_section["min_price_tick_size"])), + min_quantity_tick_size=Decimal(str(configuration_section["min_quantity_tick_size"])), + ) + + derivative_markets[market.id] = market + + elif section_name != "DEFAULT": + token = Token( + name=section_name, + symbol=section_name, + denom=configuration_section["peggy_denom"], + address="", + decimals=int(configuration_section["decimals"]), + logo="", + updated=-1, + ) + + tokens[token.symbol] = token + + self.tokens = tokens + self.spot_markets = spot_markets + self.derivative_markets = derivative_markets + self.binary_option_markets = dict() + + def _initialize_markets_and_tokens_from_files(self): + config: ConfigParser = constant.CONFIGS[self.network] + spot_markets = dict() + derivative_markets = dict() + tokens = dict() + + for section_name, configuration_section in config.items(): + if section_name.startswith("0x"): + description = configuration_section["description"] + + quote_token = Token( + name="", + symbol="", + denom="", + address="", + decimals=int(configuration_section["quote"]), + logo="", + updated=-1, + ) + + if "Spot" in description: + base_token = Token( + name="", + symbol="", + denom="", + address="", + decimals=int(configuration_section["base"]), + logo="", + updated=-1, + ) + + market = SpotMarket( + id=section_name, + status="", + ticker=description, + base_token=base_token, + quote_token=quote_token, + maker_fee_rate=None, + taker_fee_rate=None, + service_provider_fee=None, + min_price_tick_size=Decimal(str(configuration_section["min_price_tick_size"])), + min_quantity_tick_size=Decimal(str(configuration_section["min_quantity_tick_size"])) + ) + spot_markets[market.id] = market + else: + market = DerivativeMarket( + id=section_name, + status="", + ticker=description, + oracle_base="", + oracle_quote="", + oracle_type="", + oracle_scale_factor=1, + initial_margin_ratio=None, + maintenance_margin_ratio=None, + quote_token=quote_token, + maker_fee_rate=None, + taker_fee_rate=None, + service_provider_fee=None, + min_price_tick_size=Decimal(str(configuration_section["min_price_tick_size"])), + min_quantity_tick_size=Decimal(str(configuration_section["min_quantity_tick_size"])), + ) + + derivative_markets[market.id] = market + + elif section_name != "DEFAULT": + token = Token( + name=section_name, + symbol=section_name, + denom=configuration_section["peggy_denom"], + address="", + decimals=int(configuration_section["decimals"]), + logo="", + updated=-1, + ) + + tokens[token.symbol] = token + + self.tokens = tokens + self.spot_markets = spot_markets + self.derivative_markets = derivative_markets + self.binary_option_markets = dict() diff --git a/pyinjective/constant.py b/pyinjective/constant.py index a179a6aa..f08bdf63 100644 --- a/pyinjective/constant.py +++ b/pyinjective/constant.py @@ -1,9 +1,13 @@ import os from configparser import ConfigParser +from warnings import warn MAX_CLIENT_ID_LENGTH = 128 MAX_DATA_SIZE = 256 MAX_MEMO_CHARACTERS = 256 +ADDITIONAL_CHAIN_FORMAT_DECIMALS = 18 +TICKER_TOKENS_SEPARATOR = "/" +INJ_DENOM = "inj" devnet_config = ConfigParser() devnet_config.read(os.path.join(os.path.dirname(__file__), 'denoms_devnet.ini')) @@ -14,6 +18,13 @@ mainnet_config = ConfigParser() mainnet_config.read(os.path.join(os.path.dirname(__file__), 'denoms_mainnet.ini')) +CONFIGS = { + "devnet": devnet_config, + "testnet": testnet_config, + "mainnet": mainnet_config, +} + + class Denom: def __init__( self, @@ -23,6 +34,7 @@ def __init__( min_price_tick_size: float, min_quantity_tick_size: float ): + self.description = description self.base = base self.quote = quote diff --git a/pyinjective/core/__init__.py b/pyinjective/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pyinjective/core/market.py b/pyinjective/core/market.py new file mode 100644 index 00000000..9a42b88c --- /dev/null +++ b/pyinjective/core/market.py @@ -0,0 +1,151 @@ +from dataclasses import dataclass +from decimal import Decimal +from typing import Optional + +from pyinjective.constant import ADDITIONAL_CHAIN_FORMAT_DECIMALS, Denom +from pyinjective.core.token import Token + + +@dataclass(eq=True, frozen=True) +class SpotMarket: + id: str + status: str + ticker: str + base_token: Token + quote_token: Token + maker_fee_rate: Decimal + taker_fee_rate: Decimal + service_provider_fee: Decimal + min_price_tick_size: Decimal + min_quantity_tick_size: Decimal + + def quantity_to_chain_format(self, human_readable_value: Decimal) -> Decimal: + chain_formatted_value = human_readable_value * Decimal(f"1e{self.base_token.decimals}") + quantized_value = chain_formatted_value // self.min_quantity_tick_size * self.min_quantity_tick_size + extended_chain_formatted_value = quantized_value * Decimal(f"1e{ADDITIONAL_CHAIN_FORMAT_DECIMALS}") + + return extended_chain_formatted_value + + def price_to_chain_format(self, human_readable_value: Decimal) -> Decimal: + decimals = self.quote_token.decimals - self.base_token.decimals + chain_formatted_value = human_readable_value * Decimal(f"1e{decimals}") + quantized_value = (chain_formatted_value // self.min_price_tick_size) * self.min_price_tick_size + extended_chain_formatted_value = quantized_value * Decimal(f"1e{ADDITIONAL_CHAIN_FORMAT_DECIMALS}") + + return extended_chain_formatted_value + +@dataclass(eq=True, frozen=True) +class DerivativeMarket: + id: str + status: str + ticker: str + oracle_base: str + oracle_quote: str + oracle_type: str + oracle_scale_factor: int + initial_margin_ratio: Decimal + maintenance_margin_ratio: Decimal + quote_token: Token + maker_fee_rate: Decimal + taker_fee_rate: Decimal + service_provider_fee: Decimal + min_price_tick_size: Decimal + min_quantity_tick_size: Decimal + + def quantity_to_chain_format(self, human_readable_value: Decimal) -> Decimal: + # Derivative markets do not have a base market to provide the number of decimals + chain_formatted_value = human_readable_value + quantized_value = chain_formatted_value // self.min_quantity_tick_size * self.min_quantity_tick_size + extended_chain_formatted_value = quantized_value * Decimal(f"1e{ADDITIONAL_CHAIN_FORMAT_DECIMALS}") + + return extended_chain_formatted_value + + def price_to_chain_format(self, human_readable_value: Decimal) -> Decimal: + decimals = self.quote_token.decimals + chain_formatted_value = human_readable_value * Decimal(f"1e{decimals}") + quantized_value = (chain_formatted_value // self.min_price_tick_size) * self.min_price_tick_size + extended_chain_formatted_value = quantized_value * Decimal(f"1e{ADDITIONAL_CHAIN_FORMAT_DECIMALS}") + + return extended_chain_formatted_value + + def margin_to_chain_format(self, human_readable_value: Decimal) -> Decimal: + decimals = self.quote_token.decimals + chain_formatted_value = human_readable_value * Decimal(f"1e{decimals}") + quantized_value = (chain_formatted_value // self.min_quantity_tick_size) * self.min_quantity_tick_size + extended_chain_formatted_value = quantized_value * Decimal(f"1e{ADDITIONAL_CHAIN_FORMAT_DECIMALS}") + + return extended_chain_formatted_value + + def calculate_margin_in_chain_format( + self, + human_readable_quantity: Decimal, + human_readable_price: Decimal, + leverage: Decimal + ) -> Decimal: + chain_formatted_quantity = human_readable_quantity + chain_formatted_price = human_readable_price * Decimal(f"1e{self.quote_token.decimals}") + margin = (chain_formatted_price * chain_formatted_quantity) / leverage + # We are using the min_quantity_tick_size to quantize the margin because that is the way margin is validated + # in the chain (it might be changed to a min_notional in the future) + quantized_margin = (margin // self.min_quantity_tick_size) * self.min_quantity_tick_size + extended_chain_formatted_margin = quantized_margin * Decimal(f"1e{ADDITIONAL_CHAIN_FORMAT_DECIMALS}") + + return extended_chain_formatted_margin + +@dataclass(eq=True, frozen=True) +class BinaryOptionMarket: + id: str + status: str + ticker: str + oracle_symbol: str + oracle_provider: str + oracle_type: str + oracle_scale_factor: int + expiration_timestamp: int + settlement_timestamp: int + quote_token: Token + maker_fee_rate: Decimal + taker_fee_rate: Decimal + service_provider_fee: Decimal + min_price_tick_size: Decimal + min_quantity_tick_size: Decimal + + def quantity_to_chain_format(self, human_readable_value: Decimal, special_denom: Optional[Denom] = None) -> Decimal: + # Binary option markets do not have a base market to provide the number of decimals + decimals = 0 if special_denom is None else special_denom.base + min_quantity_tick_size = self.min_quantity_tick_size if special_denom is None else special_denom.min_quantity_tick_size + chain_formatted_value = human_readable_value * Decimal(f"1e{decimals}") + quantized_value = chain_formatted_value // min_quantity_tick_size * min_quantity_tick_size + extended_chain_formatted_value = quantized_value * Decimal(f"1e{ADDITIONAL_CHAIN_FORMAT_DECIMALS}") + + return extended_chain_formatted_value + + def price_to_chain_format(self, human_readable_value: Decimal, special_denom: Optional[Denom] = None) -> Decimal: + decimals = self.quote_token.decimals if special_denom is None else special_denom.quote + min_price_tick_size = self.min_price_tick_size if special_denom is None else special_denom.min_price_tick_size + chain_formatted_value = human_readable_value * Decimal(f"1e{decimals}") + quantized_value = (chain_formatted_value // min_price_tick_size) * min_price_tick_size + extended_chain_formatted_value = quantized_value * Decimal(f"1e{ADDITIONAL_CHAIN_FORMAT_DECIMALS}") + + return extended_chain_formatted_value + + def calculate_margin_in_chain_format( + self, + human_readable_quantity: Decimal, + human_readable_price: Decimal, + is_buy: bool, + special_denom: Optional[Denom] = None, + ) -> Decimal: + quantity_decimals = 0 if special_denom is None else special_denom.base + price_decimals = self.quote_token.decimals if special_denom is None else special_denom.quote + min_quantity_tick_size = self.min_quantity_tick_size if special_denom is None else special_denom.min_quantity_tick_size + price = human_readable_price if is_buy else 1 - human_readable_price + chain_formatted_quantity = human_readable_quantity * Decimal(f"1e{quantity_decimals}") + chain_formatted_price = price * Decimal(f"1e{price_decimals}") + margin = (chain_formatted_price * chain_formatted_quantity) + # We are using the min_quantity_tick_size to quantize the margin because that is the way margin is validated + # in the chain (it might be changed to a min_notional in the future) + quantized_margin = (margin // min_quantity_tick_size) * min_quantity_tick_size + extended_chain_formatted_margin = quantized_margin * Decimal(f"1e{ADDITIONAL_CHAIN_FORMAT_DECIMALS}") + + return extended_chain_formatted_margin \ No newline at end of file diff --git a/pyinjective/core/token.py b/pyinjective/core/token.py new file mode 100644 index 00000000..c6af6c0d --- /dev/null +++ b/pyinjective/core/token.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass +from decimal import Decimal + +@dataclass(eq=True, frozen=True) +class Token: + name: str + symbol: str + denom: str + address: str + decimals: int + logo: str + updated: int + + def chain_formatted_value(self, human_readable_value: Decimal) -> Decimal: + return human_readable_value * Decimal(f"1e{self.decimals}") diff --git a/pyinjective/denoms_devnet.ini b/pyinjective/denoms_devnet.ini index 7b2df629..4e4f2797 100644 --- a/pyinjective/denoms_devnet.ini +++ b/pyinjective/denoms_devnet.ini @@ -92,7 +92,7 @@ min_display_quantity_tick_size = 0.001 description = 'Devnet 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 = 100000 min_display_quantity_tick_size = 1e-13 diff --git a/pyinjective/denoms_mainnet.ini b/pyinjective/denoms_mainnet.ini index 000b5415..9a5fca85 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 @@ -664,7 +655,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 84edee96..04fb4bba 100644 --- a/pyinjective/denoms_testnet.ini +++ b/pyinjective/denoms_testnet.ini @@ -36,7 +36,7 @@ min_display_quantity_tick_size = 1e-13 [0x1c315bd2cfcc769a8d8eca49ce7b1bc5fb0353bfcb9fa82895fe0c1c2a62306e] description = 'Testnet Spot WBTC/USDT' -base = 6 +base = 8 quote = 6 min_price_tick_size = 0.00001 min_display_price_tick_size = 0.001 @@ -52,29 +52,11 @@ min_display_price_tick_size = 1e-05 min_quantity_tick_size = 100000 min_display_quantity_tick_size = 0.1 -[0xe112199d9ee44ceb2697ea0edd1cd422223c105f3ed2bdf85223d3ca59f5909a] +[0x17ef48032cb24375ba7c2e39f384e56433bcab20cbee9a7357e4cba2eb00abe6] 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/pyinjective/fetch_metadata.py b/pyinjective/utils/fetch_metadata.py similarity index 97% rename from pyinjective/fetch_metadata.py rename to pyinjective/utils/fetch_metadata.py index 1abbd815..a1a00c51 100644 --- a/pyinjective/fetch_metadata.py +++ b/pyinjective/utils/fetch_metadata.py @@ -96,12 +96,12 @@ async def fetch_denom(network) -> str: async def main() -> None: testnet = Network.testnet() data = await fetch_denom(testnet) - with open("denoms_testnet.ini", "w") as text_file: + with open("../denoms_testnet.ini", "w") as text_file: text_file.write(data) mainnet = Network.mainnet() data = await fetch_denom(mainnet) - with open("denoms_mainnet.ini", "w") as text_file: + with open("../denoms_mainnet.ini", "w") as text_file: text_file.write(data) if __name__ == '__main__': diff --git a/pyinjective/utils/metadata_validation.py b/pyinjective/utils/metadata_validation.py new file mode 100644 index 00000000..3ef03209 --- /dev/null +++ b/pyinjective/utils/metadata_validation.py @@ -0,0 +1,106 @@ +import asyncio +from decimal import Decimal +from typing import Any, List, Tuple + +import pyinjective.constant as constant +from pyinjective.async_client import AsyncClient +from pyinjective.constant import Network +from pyinjective.core.market import SpotMarket, DerivativeMarket, BinaryOptionMarket + + +def find_metadata_inconsistencies(network: Network) -> Tuple[List[Any]]: + client = AsyncClient(network, insecure=False) + ini_config = constant.CONFIGS[network.string()] + + spot_markets = asyncio.get_event_loop().run_until_complete(client.all_spot_markets()) + derivative_markets = asyncio.get_event_loop().run_until_complete(client.all_derivative_markets()) + binary_option_markets = asyncio.get_event_loop().run_until_complete(client.all_binary_option_markets()) + all_tokens = asyncio.get_event_loop().run_until_complete(client.all_tokens()) + + markets_not_found = [] + markets_with_diffs = [] + peggy_denoms_not_found = [] + peggy_denoms_with_diffs = [] + + for config_key in ini_config: + if config_key.startswith("0x"): + denom = constant.Denom.load_market(network=network.string(), market_id=config_key) + if config_key in spot_markets: + market: SpotMarket = spot_markets[config_key] + if (market.base_token.decimals != denom.base + or market.quote_token.decimals != denom.quote + or market.min_price_tick_size != Decimal(str(denom.min_price_tick_size)) + or market.min_quantity_tick_size != Decimal(str(denom.min_quantity_tick_size))): + markets_with_diffs.append([ + {"denom-market": config_key, "base_decimals": denom.base, "quote_decimals": denom.quote, + "min_quantity_tick_size": denom.min_quantity_tick_size, + "min_price_tick_size": denom.min_price_tick_size}, + {"newer-market": market.id, "base_decimals": market.base_token.decimals, + "quote_decimals": market.quote_token.decimals, + "min_quantity_tick_size": float(market.min_quantity_tick_size), + "min_price_tick_size": float(market.min_price_tick_size), "ticker": market.ticker}, + ]) + elif config_key in derivative_markets: + market: DerivativeMarket = derivative_markets[config_key] + if (market.quote_token.decimals != denom.quote + or market.min_price_tick_size != Decimal(str(denom.min_price_tick_size)) + or market.min_quantity_tick_size != Decimal(str(denom.min_quantity_tick_size))): + markets_with_diffs.append([ + {"denom-market": config_key, "quote_decimals": denom.quote, + "min_quantity_tick_size": denom.min_quantity_tick_size, + "min_price_tick_size": denom.min_price_tick_size}, + {"newer-market": market.id, "quote_decimals": market.quote_token.decimals, + "min_quantity_tick_size": float(market.min_quantity_tick_size), + "min_price_tick_size": float(market.min_price_tick_size), "ticker": market.ticker}, + ]) + elif config_key in binary_option_markets: + market: BinaryOptionMarket = binary_option_markets[config_key] + if (market.quote_token.decimals != denom.quote + or market.min_price_tick_size != Decimal(str(denom.min_price_tick_size)) + or market.min_quantity_tick_size != Decimal(str(denom.min_quantity_tick_size))): + markets_with_diffs.append([ + {"denom-market": config_key, "quote_decimals": denom.quote, + "min_quantity_tick_size": denom.min_quantity_tick_size, + "min_price_tick_size": denom.min_price_tick_size}, + {"newer-market": market.id, "quote_decimals": market.quote_token.decimals, + "min_quantity_tick_size": float(market.min_quantity_tick_size), + "min_price_tick_size": float(market.min_price_tick_size), "ticker": market.ticker}, + ]) + else: + markets_not_found.append({"denom-market": config_key, "description": denom.description}) + + elif config_key == "DEFAULT": + continue + else: + # the configuration is a token + peggy_denom, decimals = constant.Denom.load_peggy_denom(network=network.string(), symbol=config_key) + if config_key in all_tokens: + token = all_tokens[config_key] + if (token.denom != peggy_denom + or token.decimals != decimals): + peggy_denoms_with_diffs.append([ + {"denom_token": config_key, "peggy_denom": peggy_denom, "decimals": decimals}, + {"newer_token": token.symbol, "peggy_denom": token.denom, "decimals": token.decimals} + ]) + else: + peggy_denoms_not_found.append( + {"denom_token": config_key, "peggy_denom": peggy_denom, "decimals": decimals}) + + return markets_with_diffs, markets_not_found, peggy_denoms_with_diffs, peggy_denoms_not_found + + +def print_metadata_mismatches(network: Network): + markets_with_diffs, markets_not_found, peggy_denoms_with_diffs, peggy_denoms_not_found = find_metadata_inconsistencies( + network=network) + + for diff_pair in markets_with_diffs: + print(f"{diff_pair[0]}\n{diff_pair[1]}") + print(f"\n\n") + for missing_market in markets_not_found: + print(f"{missing_market}") + print(f"\n\n") + for diff_token in peggy_denoms_with_diffs: + print(f"{diff_token[0]}\n{diff_token[1]}") + print(f"\n\n") + for missing_peggy in peggy_denoms_not_found: + print(f"{missing_peggy}") diff --git a/pyinjective/utils/utils.py b/pyinjective/utils/utils.py deleted file mode 100644 index 61a02472..00000000 --- a/pyinjective/utils/utils.py +++ /dev/null @@ -1,108 +0,0 @@ -from decimal import Decimal -from math import floor -from typing import Union - -""" -One thing you may need to pay more attention to is how to deal with decimals on the Injective Exchange. -Different cryptocurrencies may require different decimal precisions. More specifically, ERC-20 tokens(e.g. INJ) have 18 decimals whereas USDT/USC have 6 decimals. -So in our system that means ** having 1 INJ is 1e18 inj ** and that ** 1 USDT is actually 100000 peggy0xdac17f958d2ee523a2206206994597c13d831ec7**. - -For spot markets, a price reflects the ** relative exchange rate ** between two tokens. -If the tokens have the same decimal scale, that's great since the prices become interpretable e.g. USDT/USDC (both have 6 decimals e.g. for USDT https://etherscan.io/address/0xdac17f958d2ee523a2206206994597c13d831ec7#readContract) -or MATIC/INJ (both have 18 decimals) since the decimals cancel out. -Prices however start to look wonky once you have exchanges between two tokens of different decimals, which unfortunately is most pairs with USDT or USDC denominations e.g. INJ/USDT. -As such, I've created some simple utility functions by keeping a hardcoded dictionary in injective-py and you can also achieve such utilities by yourself -(e.g. you can use external API like Alchemy's getTokenMetadata to fetch decimal of base and quote asset). - -So for INJ/USDT of 6.9, the price you end up getting is 6.9*10 ^ (6 - 18) = 6.9e-12. Note that this market also happens to have a MinPriceTickSize of 1e-15. -This makes sense since since it's defining th3e minimum price increment of the relative exchange of INJ to USDT. - -Note that this market also happens to have a MinQuantityTickSize of 1e15. -This also makes sense since it refers to the minimum INJ quantity tick size each order must have, which is 1e15/1e18 = 0.001 INJ. - -""" - -def spot_price_to_backend(price, denom) -> int: - price_tick_size = denom.min_price_tick_size - scale_price = Decimal(18 + denom.quote - denom.base) - exchange_price = floor_to(price, price_tick_size) * pow(Decimal(10), scale_price) - return int(exchange_price) - -def spot_quantity_to_backend(quantity, denom) -> int: - quantity_tick_size = float(denom.min_quantity_tick_size) / pow(10, denom.base) - scale_quantity = Decimal(18 + denom.base) - exchange_quantity = floor_to(quantity, quantity_tick_size) * pow(Decimal(10), scale_quantity) - return int(exchange_quantity) - -def derivative_price_to_backend(price, denom) -> int: - price_tick_size = Decimal(denom.min_price_tick_size) / pow(10, denom.quote) - exchange_price = floor_to(price, float(price_tick_size)) * pow(10, 18 + denom.quote) - return int(exchange_price) - -def binary_options_price_to_backend(price, denom) -> int: - price_tick_size = Decimal(denom.min_price_tick_size) / pow(10, denom.quote) - exchange_price = floor_to(price, float(price_tick_size)) * pow(10, 18 + denom.quote) - return int(exchange_price) - -def derivative_quantity_to_backend(quantity, denom) -> int: - quantity_tick_size = float(denom.min_quantity_tick_size) / pow(10, denom.base) - scale_quantity = Decimal(18 + denom.base) - exchange_quantity = floor_to(quantity, quantity_tick_size) * pow(Decimal(10), scale_quantity) - return int(exchange_quantity) - -def binary_options_quantity_to_backend(quantity, denom) -> int: - quantity_tick_size = float(denom.min_quantity_tick_size) / pow(10, denom.base) - scale_quantity = Decimal(18 + denom.base) - exchange_quantity = floor_to(quantity, quantity_tick_size) * pow(Decimal(10), scale_quantity) - return int(exchange_quantity) - -def derivative_margin_to_backend(price, quantity, leverage, denom) -> int: - chain_format_price = Decimal(str(price)) * Decimal(f"1e{denom.quote}") - chain_format_quantity = Decimal(str(quantity)) * Decimal(f"1e{denom.base}") - decimal_leverage = Decimal(str(leverage)) - margin = (chain_format_price * chain_format_quantity) / decimal_leverage - # We are using the min_quantity_tick_size to quantize the margin because that is the way margin is validated - # in the chain (it might be changed to a min_notional in the future) - exchange_margin = floor_to(margin, denom.min_quantity_tick_size) * Decimal(f"1e18") - return int(exchange_margin) - -def binary_options_buy_margin_to_backend(price, quantity, denom) -> int: - price_tick_size = Decimal(denom.min_price_tick_size) / pow(10, denom.quote) - margin = Decimal(str(price)) * Decimal(str(quantity)) - exchange_margin = floor_to(margin, float(price_tick_size)) * pow(10, 18 + denom.quote) - return int(exchange_margin) - -def binary_options_sell_margin_to_backend(price, quantity, denom) -> int: - price_tick_size = Decimal(denom.min_price_tick_size) / pow(10, denom.quote) - margin = (1 - (Decimal(str(price)))) * (Decimal(str(quantity))) - exchange_margin = floor_to(margin, float(price_tick_size)) * pow(10, 18 + denom.quote) - return int(exchange_margin) - -def derivative_additional_margin_to_backend(amount, denom) -> int: - price_tick_size = float(denom.min_price_tick_size) / pow(10, denom.quote) - additional_margin = floor_to(amount, price_tick_size) * pow(10, 18 + denom.quote) - return int(additional_margin) - -def amount_to_backend(amount, decimals) -> int: - be_amount = amount * pow(10, decimals) - return int(be_amount) - -def floor_to(value: Union[float, Decimal], target: Union[float, Decimal]) -> Decimal: - value_tmp = Decimal(str(value)) - target_tmp = Decimal(str(target)) - result = int(floor(value_tmp / target_tmp)) * target_tmp - return result - -def spot_price_from_backend(price, denom) -> float: - scale = float(denom.quote - denom.base) - return float(price) / pow(10, scale) - -def spot_quantity_from_backend(quantity, denom) -> Decimal: - scale = float(denom.base) - quantity_tick_size = float(denom.min_quantity_tick_size) / pow(10, scale) - quantity = float(quantity) / pow(10, scale) - return floor_to(quantity, quantity_tick_size) - -def derivative_price_from_backend(price, denom) -> float: - scale = float(denom.quote) - return float(price) / pow(10, scale) diff --git a/run-examples.sh b/run-examples.sh index 8196e9d9..4b376ce9 100755 --- a/run-examples.sh +++ b/run-examples.sh @@ -1,5 +1,5 @@ runChainExamples(){ - examples=$(find -s examples/chain_client_examples -type f -mindepth 1 -name '*.py') + examples=$(find -s examples/chain_client -type f -mindepth 1 -name '*.py') echo "Running all chain examples..." for example in $examples do @@ -10,7 +10,7 @@ runChainExamples(){ } runExchangeExamples(){ - dirs=$(find -s examples/exchange_api_examples -type d -mindepth 1) + dirs=$(find -s examples/exchange_client -type d -mindepth 1) for dir in $dirs do examples=$(find -s $dir -type f -name '*.py') @@ -26,6 +26,8 @@ runExchangeExamples(){ done } +export PYTHONPATH=$PYTHONPATH:$(pwd) + TYPE=$1 case $TYPE in "chain") diff --git a/setup.py b/setup.py index f3c350d7..765a55bd 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" +VERSION = "0.7.1-pre" REQUIRED = [ "aiohttp", @@ -42,6 +42,7 @@ DEV_REQUIRED = [ "pytest", "pytest-asyncio", + "pytest-grpc", "request-mock", ] diff --git a/tests/async_client_tests.py b/tests/async_client_tests.py deleted file mode 100644 index 0446d9ec..00000000 --- a/tests/async_client_tests.py +++ /dev/null @@ -1,84 +0,0 @@ -import logging - -import pytest -from pyinjective.constant import Network - -from pyinjective.async_client import AsyncClient - - -class TestAsyncClient: - - def test_instance_creation_logs_session_cookie_load_info(self, caplog): - caplog.set_level(logging.INFO) - - AsyncClient( - network=Network.local(), - insecure=False, - ) - - expected_log_message_prefix = "chain session cookie loaded from disk: " - found_log = next( - (record for record in caplog.record_tuples if record[2].startswith(expected_log_message_prefix)), - None, - ) - assert(found_log is not None) - assert(found_log[0] == "pyinjective.async_client.AsyncClient") - assert(found_log[1] == logging.INFO) - - - @pytest.mark.asyncio - async def test_sync_timeout_height_logs_exception(self, caplog): - client = AsyncClient( - network=Network.local(), - insecure=False, - ) - - with caplog.at_level(logging.DEBUG): - await client.sync_timeout_height() - - expected_log_message_prefix = "error while fetching latest block, setting timeout height to 0: " - found_log = next( - (record for record in caplog.record_tuples if record[2].startswith(expected_log_message_prefix)), - None, - ) - assert (found_log is not None) - assert (found_log[0] == "pyinjective.async_client.AsyncClient") - assert (found_log[1] == logging.DEBUG) - - def test_set_cookie_logs_chain_session_cookie_saved(self, caplog): - caplog.set_level(logging.INFO) - - client = AsyncClient( - network=Network.local(), - insecure=False, - ) - - client.set_cookie(metadata=[("set-cookie", "test-value")], type="chain") - - expected_log_message_prefix = "chain session cookie saved to disk" - found_log = next( - (record for record in caplog.record_tuples if record[2].startswith(expected_log_message_prefix)), - None, - ) - assert (found_log is not None) - assert (found_log[0] == "pyinjective.async_client.AsyncClient") - assert (found_log[1] == logging.INFO) - - @pytest.mark.asyncio - async def test_get_account_logs_exception(self, caplog): - client = AsyncClient( - network=Network.local(), - insecure=False, - ) - - with caplog.at_level(logging.DEBUG): - await client.get_account(address="") - - expected_log_message_prefix = "error while fetching sequence and number " - found_log = next( - (record for record in caplog.record_tuples if record[2].startswith(expected_log_message_prefix)), - None, - ) - assert (found_log is not None) - assert (found_log[0] == "pyinjective.async_client.AsyncClient") - assert (found_log[1] == logging.DEBUG) \ No newline at end of file diff --git a/tests/composer_tests.py b/tests/composer_tests.py deleted file mode 100644 index eabef128..00000000 --- a/tests/composer_tests.py +++ /dev/null @@ -1,183 +0,0 @@ -import logging - -import pytest - -from pyinjective.composer import Composer -from pyinjective.constant import Denom, Network - - -class TestComposer: - - @pytest.fixture - def inj_usdt_market_id(self): - return "0xa508cb32923323679f29a032c70342c147c17d0145625922b0ef22e955c844c0" - - def test_spot_order_creation_logs_market_data_loaded_info(self, caplog, inj_usdt_market_id): - caplog.set_level(logging.INFO) - - composer = Composer(network=Network.devnet().string()) - - composer.SpotOrder( - market_id=inj_usdt_market_id, - subaccount_id="", - fee_recipient="", - price=1.0, - quantity=1.0, - ) - - expected_log_message_prefix = "Loaded market metadata for: 'Devnet Spot INJ/USDT'" - expected_log_record = ("pyinjective.composer.Composer", logging.INFO, expected_log_message_prefix) - assert(expected_log_record in caplog.record_tuples) - - def test_derivative_order_creation_logs_market_data_loaded_info(self, caplog, inj_usdt_market_id): - caplog.set_level(logging.INFO) - - composer = Composer(network=Network.devnet().string()) - - composer.DerivativeOrder( - market_id=inj_usdt_market_id, - subaccount_id="", - fee_recipient="", - price=1.0, - quantity=1.0, - leverage=1.0, - ) - - expected_log_message_prefix = "Loaded market metadata for: 'Devnet Spot INJ/USDT'" - expected_log_record = ("pyinjective.composer.Composer", logging.INFO, expected_log_message_prefix) - assert(expected_log_record in caplog.record_tuples) - - def test_binary_order_creation_logs_market_data_loaded_info(self, caplog, inj_usdt_market_id): - caplog.set_level(logging.INFO) - - composer = Composer(network=Network.devnet().string()) - - composer.BinaryOptionsOrder( - market_id=inj_usdt_market_id, - subaccount_id="", - fee_recipient="", - price=1.0, - quantity=1.0, - ) - - expected_log_message_prefix = "Loaded market metadata for: 'Devnet Spot INJ/USDT'" - expected_log_record = ("pyinjective.composer.Composer", logging.INFO, expected_log_message_prefix) - assert(expected_log_record in caplog.record_tuples) - - def test_msg_send_logs_market_data_loaded_info(self, caplog): - caplog.set_level(logging.INFO) - - composer = Composer(network=Network.devnet().string()) - - composer.MsgSend( - from_address="", - to_address="", - amount=1.0, - denom="INJ", - ) - - expected_log_message_prefix = f"Loaded send symbol INJ (inj) with decimals = 18" - expected_log_record = ("pyinjective.composer.Composer", logging.INFO, expected_log_message_prefix) - assert(expected_log_record in caplog.record_tuples) - - def test_msg_deposit_logs_market_data_loaded_info(self, caplog): - caplog.set_level(logging.INFO) - - composer = Composer(network=Network.devnet().string()) - - composer.MsgDeposit( - sender="", - subaccount_id="", - amount=1.0, - denom="INJ", - ) - - expected_log_message_prefix = f"Loaded deposit symbol INJ (inj) with decimals = 18" - expected_log_record = ("pyinjective.composer.Composer", logging.INFO, expected_log_message_prefix) - assert(expected_log_record in caplog.record_tuples) - - def test_msg_withdraw_logs_market_data_loaded_info(self, caplog): - caplog.set_level(logging.INFO) - - composer = Composer(network=Network.devnet().string()) - - composer.MsgWithdraw( - sender="", - subaccount_id="", - amount=1.0, - denom="INJ", - ) - - expected_log_message_prefix = f"Loaded withdrawal symbol INJ (inj) with decimals = 18" - expected_log_record = ("pyinjective.composer.Composer", logging.INFO, expected_log_message_prefix) - assert(expected_log_record in caplog.record_tuples) - - def test_msg_external_transfer_logs_market_data_loaded_info(self, caplog): - caplog.set_level(logging.INFO) - - composer = Composer(network=Network.devnet().string()) - - composer.MsgExternalTransfer( - sender="", - source_subaccount_id="", - destination_subaccount_id="", - amount=1.0, - denom="INJ", - ) - - expected_log_message_prefix = f"Loaded send symbol INJ (inj) with decimals = 18" - expected_log_record = ("pyinjective.composer.Composer", logging.INFO, expected_log_message_prefix) - assert(expected_log_record in caplog.record_tuples) - - def test_msg_send_to_eth_logs_market_data_loaded_info(self, caplog): - caplog.set_level(logging.INFO) - - composer = Composer(network=Network.devnet().string()) - - composer.MsgSendToEth( - denom="INJ", - sender="", - eth_dest="", - amount=1.0, - bridge_fee=1.0, - ) - - expected_log_message_prefix = f"Loaded withdrawal symbol INJ (inj) with decimals = 18" - expected_log_record = ("pyinjective.composer.Composer", logging.INFO, expected_log_message_prefix) - assert(expected_log_record in caplog.record_tuples) - - def test_msg_create_insurance_fund_logs_market_data_loaded_info(self, caplog): - caplog.set_level(logging.INFO) - - composer = Composer(network=Network.devnet().string()) - - composer.MsgCreateInsuranceFund( - sender="", - ticker="", - quote_denom="INJ", - oracle_base="", - oracle_quote="", - oracle_type=1, - expiry=1, - initial_deposit=1, - ) - - expected_log_message_prefix = f"Loaded send symbol INJ (inj) with decimals = 18" - expected_log_record = ("pyinjective.composer.Composer", logging.INFO, expected_log_message_prefix) - assert(expected_log_record in caplog.record_tuples) - - def test_msg_underwrite_logs_market_data_loaded_info(self, caplog, inj_usdt_market_id): - caplog.set_level(logging.INFO) - - composer = Composer(network=Network.devnet().string()) - - composer.MsgUnderwrite( - sender="", - market_id=inj_usdt_market_id, - quote_denom="INJ", - amount=1, - ) - - expected_log_message_prefix = f"Loaded send symbol INJ (inj) with decimals = 18" - expected_log_record = ("pyinjective.composer.Composer", logging.INFO, expected_log_message_prefix) - assert(expected_log_record in caplog.record_tuples) diff --git a/tests/core/__init__.py b/tests/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/core/test_market.py b/tests/core/test_market.py new file mode 100644 index 00000000..1038c51b --- /dev/null +++ b/tests/core/test_market.py @@ -0,0 +1,213 @@ +import pytest +from decimal import Decimal + +from pyinjective.constant import Denom + +from pyinjective.core.market import BinaryOptionMarket, DerivativeMarket, SpotMarket +from tests.model_fixtures.markets_fixtures import ( + first_match_bet_market, + inj_token, + usdt_token, + usdt_perp_token, + inj_usdt_spot_market, + btc_usdt_perp_market, +) + + +class TestSpotMarket: + + def test_convert_quantity_to_chain_format(self, inj_usdt_spot_market: SpotMarket): + original_quantity = Decimal("123.456789") + + chain_value = inj_usdt_spot_market.quantity_to_chain_format(human_readable_value=original_quantity) + expected_value = original_quantity * Decimal(f"1e{inj_usdt_spot_market.base_token.decimals}") + quantized_value = ((expected_value // inj_usdt_spot_market.min_quantity_tick_size) + * inj_usdt_spot_market.min_quantity_tick_size) + quantized_chain_format_value = quantized_value * Decimal(f"1e18") + + assert (quantized_chain_format_value == chain_value) + + def test_convert_price_to_chain_format(self, inj_usdt_spot_market: SpotMarket): + original_quantity = Decimal("123.456789") + + chain_value = inj_usdt_spot_market.price_to_chain_format(human_readable_value=original_quantity) + price_decimals = inj_usdt_spot_market.quote_token.decimals - inj_usdt_spot_market.base_token.decimals + expected_value = original_quantity * Decimal(f"1e{price_decimals}") + quantized_value = ((expected_value // inj_usdt_spot_market.min_price_tick_size) + * inj_usdt_spot_market.min_price_tick_size) + quantized_chain_format_value = quantized_value * Decimal(f"1e18") + + assert (quantized_chain_format_value == chain_value) + + +class TestDerivativeMarket: + + def test_convert_quantity_to_chain_format(self, btc_usdt_perp_market: DerivativeMarket): + original_quantity = Decimal("123.456789") + + chain_value = btc_usdt_perp_market.quantity_to_chain_format(human_readable_value=original_quantity) + quantized_value = ((original_quantity // btc_usdt_perp_market.min_quantity_tick_size) + * btc_usdt_perp_market.min_quantity_tick_size) + quantized_chain_format_value = quantized_value * Decimal(f"1e18") + + assert (quantized_chain_format_value == chain_value) + + def test_convert_price_to_chain_format(self, btc_usdt_perp_market: DerivativeMarket): + original_quantity = Decimal("123.456789") + + chain_value = btc_usdt_perp_market.price_to_chain_format(human_readable_value=original_quantity) + price_decimals = btc_usdt_perp_market.quote_token.decimals + expected_value = original_quantity * Decimal(f"1e{price_decimals}") + quantized_value = ((expected_value // btc_usdt_perp_market.min_price_tick_size) + * btc_usdt_perp_market.min_price_tick_size) + quantized_chain_format_value = quantized_value * Decimal(f"1e18") + + assert (quantized_chain_format_value == chain_value) + + def test_convert_margin_to_chain_format(self, btc_usdt_perp_market: DerivativeMarket): + original_quantity = Decimal("123.456789") + + chain_value = btc_usdt_perp_market.margin_to_chain_format(human_readable_value=original_quantity) + margin_decimals = btc_usdt_perp_market.quote_token.decimals + expected_value = original_quantity * Decimal(f"1e{margin_decimals}") + quantized_value = ((expected_value // btc_usdt_perp_market.min_quantity_tick_size) + * btc_usdt_perp_market.min_quantity_tick_size) + quantized_chain_format_value = quantized_value * Decimal(f"1e18") + + assert (quantized_chain_format_value == chain_value) + +class TestBinaryOptionMarket: + + def test_convert_quantity_to_chain_format_with_fixed_denom(self, first_match_bet_market: BinaryOptionMarket): + original_quantity = Decimal("123.456789") + fixed_denom = Denom( + description="Fixed denom", + base=2, + quote=4, + min_quantity_tick_size=100, + min_price_tick_size=10000, + ) + + chain_value = first_match_bet_market.quantity_to_chain_format( + human_readable_value=original_quantity, + special_denom=fixed_denom + ) + chain_formatted_quantity = original_quantity * Decimal(f"1e{fixed_denom.base}") + quantized_value = ((chain_formatted_quantity // Decimal(str(fixed_denom.min_quantity_tick_size))) + * Decimal(str(fixed_denom.min_quantity_tick_size))) + quantized_chain_format_value = quantized_value * Decimal(f"1e18") + + assert (quantized_chain_format_value == chain_value) + + def test_convert_quantity_to_chain_format_without_fixed_denom(self, first_match_bet_market: BinaryOptionMarket): + original_quantity = Decimal("123.456789") + + chain_value = first_match_bet_market.quantity_to_chain_format( + human_readable_value=original_quantity, + ) + quantized_value = ((original_quantity // first_match_bet_market.min_quantity_tick_size) + * first_match_bet_market.min_quantity_tick_size) + quantized_chain_format_value = quantized_value * Decimal(f"1e18") + + assert (quantized_chain_format_value == chain_value) + + def test_convert_price_to_chain_format_with_fixed_denom(self, first_match_bet_market: BinaryOptionMarket): + original_quantity = Decimal("123.456789") + fixed_denom = Denom( + description="Fixed denom", + base=2, + quote=4, + min_quantity_tick_size=100, + min_price_tick_size=10000, + ) + + chain_value = first_match_bet_market.price_to_chain_format( + human_readable_value=original_quantity, + special_denom=fixed_denom, + ) + price_decimals = fixed_denom.quote + expected_value = original_quantity * Decimal(f"1e{price_decimals}") + quantized_value = ((expected_value // Decimal(str(fixed_denom.min_price_tick_size))) + * Decimal(str(fixed_denom.min_price_tick_size))) + quantized_chain_format_value = quantized_value * Decimal(f"1e18") + + assert (quantized_chain_format_value == chain_value) + + def test_convert_price_to_chain_format_without_fixed_denom(self, first_match_bet_market: BinaryOptionMarket): + original_quantity = Decimal("123.456789") + + chain_value = first_match_bet_market.price_to_chain_format(human_readable_value=original_quantity) + price_decimals = first_match_bet_market.quote_token.decimals + expected_value = original_quantity * Decimal(f"1e{price_decimals}") + quantized_value = ((expected_value // first_match_bet_market.min_price_tick_size) + * first_match_bet_market.min_price_tick_size) + quantized_chain_format_value = quantized_value * Decimal(f"1e18") + + assert (quantized_chain_format_value == chain_value) + + def test_calculate_margin_for_buy_with_fixed_denom(self, first_match_bet_market: BinaryOptionMarket): + original_quantity = Decimal("123.456789") + original_price = Decimal("0.6789") + fixed_denom = Denom( + description="Fixed denom", + base=2, + quote=4, + min_quantity_tick_size=100, + min_price_tick_size=10000, + ) + + chain_value = first_match_bet_market.calculate_margin_in_chain_format( + human_readable_quantity=original_quantity, + human_readable_price=original_price, + is_buy=True, + special_denom=fixed_denom, + ) + + quantity_decimals = fixed_denom.base + price_decimals = fixed_denom.quote + expected_quantity = original_quantity * Decimal(f"1e{quantity_decimals}") + expected_price = original_price * Decimal(f"1e{price_decimals}") + expected_margin = expected_quantity * expected_price + quantized_margin = ((expected_margin // Decimal(str(fixed_denom.min_quantity_tick_size))) + * Decimal(str(fixed_denom.min_quantity_tick_size))) + quantized_chain_format_margin = quantized_margin * Decimal(f"1e18") + + assert (quantized_chain_format_margin == chain_value) + + def test_calculate_margin_for_buy_without_fixed_denom(self, first_match_bet_market: BinaryOptionMarket): + original_quantity = Decimal("123.456789") + original_price = Decimal("0.6789") + + chain_value = first_match_bet_market.calculate_margin_in_chain_format( + human_readable_quantity=original_quantity, + human_readable_price=original_price, + is_buy=True, + ) + + price_decimals = first_match_bet_market.quote_token.decimals + expected_price = original_price * Decimal(f"1e{price_decimals}") + expected_margin = original_quantity * expected_price + quantized_margin = ((expected_margin // Decimal(str(first_match_bet_market.min_quantity_tick_size))) + * Decimal(str(first_match_bet_market.min_quantity_tick_size))) + quantized_chain_format_margin = quantized_margin * Decimal(f"1e18") + + assert (quantized_chain_format_margin == chain_value) + + def test_calculate_margin_for_sell_without_fixed_denom(self, first_match_bet_market: BinaryOptionMarket): + original_quantity = Decimal("123.456789") + original_price = Decimal("0.6789") + + chain_value = first_match_bet_market.calculate_margin_in_chain_format( + human_readable_quantity=original_quantity, + human_readable_price=original_price, + is_buy=False, + ) + + price_decimals = first_match_bet_market.quote_token.decimals + expected_price = (Decimal(1) - original_price) * Decimal(f"1e{price_decimals}") + expected_margin = original_quantity * expected_price + quantized_margin = ((expected_margin // Decimal(str(first_match_bet_market.min_quantity_tick_size))) + * Decimal(str(first_match_bet_market.min_quantity_tick_size))) + quantized_chain_format_margin = quantized_margin * Decimal(f"1e18") + + assert (quantized_chain_format_margin == chain_value) diff --git a/tests/core/test_token.py b/tests/core/test_token.py new file mode 100644 index 00000000..66cb6cd7 --- /dev/null +++ b/tests/core/test_token.py @@ -0,0 +1,15 @@ +import pytest +from decimal import Decimal + +from tests.model_fixtures.markets_fixtures import inj_token + + +class TestToken: + + def test_chain_formatted_value(self, inj_token): + value = Decimal("1.3456") + + chain_formatted_value = inj_token.chain_formatted_value(human_readable_value=value) + expected_value = value * Decimal(f"1e{inj_token.decimals}") + + assert (chain_formatted_value == expected_value) diff --git a/tests/model_fixtures/__init__.py b/tests/model_fixtures/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/model_fixtures/markets_fixtures.py b/tests/model_fixtures/markets_fixtures.py new file mode 100644 index 00000000..cca40680 --- /dev/null +++ b/tests/model_fixtures/markets_fixtures.py @@ -0,0 +1,113 @@ +import pytest +from decimal import Decimal + +from pyinjective.core.market import BinaryOptionMarket, DerivativeMarket, SpotMarket +from pyinjective.core.token import Token + + +@pytest.fixture +def inj_token(): + token = Token( + name="Injective Protocol", + symbol="INJ", + denom="peggy0x44C21afAaF20c270EBbF5914Cfc3b5022173FEB7", + address="0xe28b3B32B6c345A34Ff64674606124Dd5Aceca30", + decimals=18, + logo="https://static.alchemyapi.io/images/assets/7226.png", + updated=1681739137644, + ) + + return token + + +@pytest.fixture +def usdt_token(): + token = Token( + name="USDT", + symbol="USDT", + denom="peggy0x87aB3B4C8661e07D6372361211B96ed4Dc36B1B5", + address="0x0000000000000000000000000000000000000000", + decimals=6, + logo="https://static.alchemyapi.io/images/assets/825.png", + updated=1681739137645, + ) + + return token + + +@pytest.fixture +def usdt_perp_token(): + token = Token( + name="USDT PERP", + symbol="USDT", + denom="peggy0xdAC17F958D2ee523a2206206994597C13D831ec7", + address="0xdAC17F958D2ee523a2206206994597C13D831ec7", + decimals=6, + logo="https://static.alchemyapi.io/images/assets/825.png", + updated=1681739137645, + ) + + return token + + +@pytest.fixture +def inj_usdt_spot_market(inj_token, usdt_token): + market = SpotMarket( + id="0x7a57e705bb4e09c88aecfc295569481dbf2fe1d5efe364651fbe72385938e9b0", + status="active", + ticker="INJ/USDT", + base_token=inj_token, + quote_token=usdt_token, + maker_fee_rate=Decimal("-0.0001"), + taker_fee_rate=Decimal("0.001"), + service_provider_fee=Decimal("0.4"), + min_price_tick_size=Decimal("0.000000000000001"), + min_quantity_tick_size=Decimal("1000000000000000"), + ) + + return market + + +@pytest.fixture +def btc_usdt_perp_market(usdt_perp_token): + market = DerivativeMarket( + id="0x4ca0f92fc28be0c9761326016b5a1a2177dd6375558365116b5bdda9abc229ce", + status="active", + ticker="BTC/USDT PERP", + oracle_base="BTC", + oracle_quote=usdt_perp_token.symbol, + oracle_type="bandibc", + oracle_scale_factor=6, + initial_margin_ratio=Decimal("0.095"), + maintenance_margin_ratio=Decimal("0.025"), + quote_token=usdt_perp_token, + maker_fee_rate=Decimal("-0.0001"), + taker_fee_rate=Decimal("0.001"), + service_provider_fee=Decimal("0.4"), + min_price_tick_size=Decimal("1000000"), + min_quantity_tick_size=Decimal("0.0001"), + ) + + return market + +@pytest.fixture +def first_match_bet_market(usdt_token): + market = BinaryOptionMarket( + id="0x230dcce315364ff6360097838701b14713e2f4007d704df20ed3d81d09eec957", + status="active", + ticker="5fdbe0b1-1707800399-WAS", + oracle_symbol="Frontrunner", + oracle_provider="Frontrunner", + oracle_type="provider", + oracle_scale_factor=6, + expiration_timestamp=1707800399, + settlement_timestamp=1707843599, + quote_token=usdt_token, + maker_fee_rate=Decimal("0"), + taker_fee_rate=Decimal("0"), + service_provider_fee=Decimal("0.4"), + min_price_tick_size=Decimal("10000"), + min_quantity_tick_size=Decimal("1"), + ) + + return market diff --git a/tests/rpc_fixtures/__init__.py b/tests/rpc_fixtures/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/rpc_fixtures/configurable_servicers.py b/tests/rpc_fixtures/configurable_servicers.py new file mode 100644 index 00000000..136491ce --- /dev/null +++ b/tests/rpc_fixtures/configurable_servicers.py @@ -0,0 +1,38 @@ +from collections import deque + +import pytest +from pyinjective.proto.exchange.injective_derivative_exchange_rpc_pb2_grpc import InjectiveDerivativeExchangeRPCServicer + +from pyinjective.proto.exchange import ( + injective_spot_exchange_rpc_pb2, + injective_derivative_exchange_rpc_pb2, +) +from pyinjective.proto.exchange.injective_spot_exchange_rpc_pb2_grpc import InjectiveSpotExchangeRPCServicer + + +class ConfigurableInjectiveSpotExchangeRPCServicer(InjectiveSpotExchangeRPCServicer): + + def __init__(self): + super().__init__() + self.markets_queue = deque() + + async def Markets(self, request: injective_spot_exchange_rpc_pb2.MarketsRequest, context=None): + return self.markets_queue.pop() + + +class ConfigurableInjectiveDerivativeExchangeRPCServicer(InjectiveDerivativeExchangeRPCServicer): + + def __init__(self): + super().__init__() + self.markets_queue = deque() + self.binary_option_markets_queue = deque() + + async def Markets(self, request: injective_derivative_exchange_rpc_pb2.MarketsRequest, context=None): + return self.markets_queue.pop() + + async def BinaryOptionsMarkets( + self, + request: injective_derivative_exchange_rpc_pb2.BinaryOptionsMarketsRequest, + context=None + ): + return self.binary_option_markets_queue.pop() diff --git a/tests/rpc_fixtures/markets_fixtures.py b/tests/rpc_fixtures/markets_fixtures.py new file mode 100644 index 00000000..b6a2d3a3 --- /dev/null +++ b/tests/rpc_fixtures/markets_fixtures.py @@ -0,0 +1,189 @@ +import pytest + + +@pytest.fixture +def inj_token_meta(): + from pyinjective.proto.exchange.injective_spot_exchange_rpc_pb2 import TokenMeta + + token = TokenMeta( + name="Injective Protocol", + address="0xe28b3B32B6c345A34Ff64674606124Dd5Aceca30", + symbol="INJ", + logo="https://static.alchemyapi.io/images/assets/7226.png", + decimals=18, + updated_at=1681739137644 + ) + + return token + +@pytest.fixture +def ape_token_meta(): + from pyinjective.proto.exchange.injective_spot_exchange_rpc_pb2 import TokenMeta + + token = TokenMeta( + name="APE", + address="0x0000000000000000000000000000000000000000", + symbol="APE", + logo="https://assets.coingecko.com/coins/images/24383/small/apecoin.jpg?1647476455", + decimals=18, + updated_at=1681739137646 + ) + + return token + +@pytest.fixture +def usdt_token_meta(): + from pyinjective.proto.exchange.injective_spot_exchange_rpc_pb2 import TokenMeta + + token = TokenMeta( + name="USDT", + address="0x0000000000000000000000000000000000000000", + symbol="USDT", + logo="https://static.alchemyapi.io/images/assets/825.png", + decimals=6, + updated_at=1681739137645 + ) + + return token + +@pytest.fixture +def usdt_token_meta_second_denom(): + from pyinjective.proto.exchange.injective_spot_exchange_rpc_pb2 import TokenMeta + + token = TokenMeta( + name="USDT Second Denom", + address="0x0000000000000000000000000000000000000000", + symbol="USDT", + logo="https://static.alchemyapi.io/images/assets/826.png", + decimals=6, + updated_at=1691739137645 + ) + + return token + + +@pytest.fixture +def usdt_perp_token_meta(): + from pyinjective.proto.exchange.injective_derivative_exchange_rpc_pb2 import TokenMeta + + token = TokenMeta( + name="Tether", + address="0xdAC17F958D2ee523a2206206994597C13D831ec7", + symbol="USDTPerp", + logo="https://static.alchemyapi.io/images/assets/825.png", + decimals=6, + updated_at=1683929869866 + ) + + return token + + +@pytest.fixture +def ape_usdt_spot_market_meta(ape_token_meta, usdt_token_meta_second_denom): + from pyinjective.proto.exchange.injective_spot_exchange_rpc_pb2 import SpotMarketInfo + + market = SpotMarketInfo( + market_id="0x8b67e705bb4e09c88aecfc295569481dbf2fe1d5efe364651fbe72385938e000", + market_status="active", + ticker="APE/USDT", + base_denom="peggy0x44C21afAaF20c270EBbF5914Cfc3b5022173FEB7", + base_token_meta=ape_token_meta, + quote_denom="factory/peggy0x87aB3B4C8661e07D6372361211B96ed4Dc300000", + quote_token_meta=usdt_token_meta_second_denom, + maker_fee_rate="-0.0001", + taker_fee_rate="0.001", + service_provider_fee="0.4", + min_price_tick_size="0.000000000000001", + min_quantity_tick_size="1000000000000000", + ) + + return market + +@pytest.fixture +def inj_usdt_spot_market_meta(inj_token_meta, usdt_token_meta): + from pyinjective.proto.exchange.injective_spot_exchange_rpc_pb2 import SpotMarketInfo + + market = SpotMarketInfo( + market_id="0x7a57e705bb4e09c88aecfc295569481dbf2fe1d5efe364651fbe72385938e9b0", + market_status="active", + ticker="INJ/USDT", + base_denom="peggy0x44C21afAaF20c270EBbF5914Cfc3b5022173FEB7", + base_token_meta=inj_token_meta, + quote_denom="peggy0x87aB3B4C8661e07D6372361211B96ed4Dc36B1B5", + quote_token_meta=usdt_token_meta, + maker_fee_rate="-0.0001", + taker_fee_rate="0.001", + service_provider_fee="0.4", + min_price_tick_size="0.000000000000001", + min_quantity_tick_size="1000000000000000", + ) + + return market + +@pytest.fixture +def btc_usdt_perp_market_meta(usdt_perp_token_meta): + from pyinjective.proto.exchange.injective_derivative_exchange_rpc_pb2 import ( + DerivativeMarketInfo, + PerpetualMarketInfo, + PerpetualMarketFunding, + ) + + perpetual_market_info = PerpetualMarketInfo( + hourly_funding_rate_cap="0.0000625", + hourly_interest_rate="0.00000416666", + next_funding_timestamp=1684764000, + funding_interval=3600, + ) + perpetual_market_funding = PerpetualMarketFunding( + cumulative_funding="6880500093.266083891331674194", + cumulative_price="-0.952642601240470199", + last_timestamp=1684763442, + ) + + market = DerivativeMarketInfo( + market_id="0x4ca0f92fc28be0c9761326016b5a1a2177dd6375558365116b5bdda9abc229ce", + market_status="active", + ticker="BTC/USDT PERP", + oracle_base="BTC", + oracle_quote="USDT", + oracle_type="bandibc", + oracle_scale_factor=6, + initial_margin_ratio="0.095", + maintenance_margin_ratio="0.025", + quote_denom="peggy0xdAC17F958D2ee523a2206206994597C13D831ec7", + quote_token_meta=usdt_perp_token_meta, + maker_fee_rate="-0.0001", + taker_fee_rate="0.001", + service_provider_fee="0.4", + is_perpetual=True, + min_price_tick_size="1000000", + min_quantity_tick_size="0.0001", + perpetual_market_info=perpetual_market_info, + perpetual_market_funding=perpetual_market_funding, + ) + + return market + +@pytest.fixture +def first_match_bet_market_meta(inj_usdt_spot_market_meta): + from pyinjective.proto.exchange.injective_derivative_exchange_rpc_pb2 import BinaryOptionsMarketInfo + + market = BinaryOptionsMarketInfo( + market_id="0x230dcce315364ff6360097838701b14713e2f4007d704df20ed3d81d09eec957", + market_status="active", + ticker="5fdbe0b1-1707800399-WAS", + oracle_symbol="Frontrunner", + oracle_provider="Frontrunner", + oracle_type="provider", + oracle_scale_factor=6, + expiration_timestamp=1707800399, + settlement_timestamp=1707843599, + quote_denom=inj_usdt_spot_market_meta.quote_denom, + maker_fee_rate="0", + taker_fee_rate="0", + service_provider_fee="0.4", + min_price_tick_size="10000", + min_quantity_tick_size="1", + ) + + return market \ No newline at end of file diff --git a/tests/test_async_client.py b/tests/test_async_client.py new file mode 100644 index 00000000..56644e9c --- /dev/null +++ b/tests/test_async_client.py @@ -0,0 +1,167 @@ +import logging + +import pytest +from pyinjective.constant import Network + +from pyinjective.async_client import AsyncClient +from pyinjective.proto.exchange import ( + injective_spot_exchange_rpc_pb2, + injective_derivative_exchange_rpc_pb2, +) +from tests.rpc_fixtures.markets_fixtures import ( + inj_token_meta, + ape_token_meta, + usdt_token_meta, + usdt_token_meta_second_denom, + usdt_perp_token_meta, + inj_usdt_spot_market_meta, + ape_usdt_spot_market_meta, + btc_usdt_perp_market_meta, + first_match_bet_market_meta, +) +from tests.rpc_fixtures.configurable_servicers import ConfigurableInjectiveDerivativeExchangeRPCServicer, \ + ConfigurableInjectiveSpotExchangeRPCServicer + + +@pytest.fixture +def spot_servicer(): + return ConfigurableInjectiveSpotExchangeRPCServicer() + + +@pytest.fixture +def derivative_servicer(): + return ConfigurableInjectiveDerivativeExchangeRPCServicer() + + +class TestAsyncClient: + + def test_instance_creation_logs_session_cookie_load_info(self, caplog): + caplog.set_level(logging.INFO) + + AsyncClient( + network=Network.local(), + insecure=False, + ) + + expected_log_message_prefix = "chain session cookie loaded from disk: " + found_log = next( + (record for record in caplog.record_tuples if record[2].startswith(expected_log_message_prefix)), + None, + ) + assert(found_log is not None) + assert(found_log[0] == "pyinjective.async_client.AsyncClient") + assert(found_log[1] == logging.INFO) + + + @pytest.mark.asyncio + async def test_sync_timeout_height_logs_exception(self, caplog): + client = AsyncClient( + network=Network.local(), + insecure=False, + ) + + with caplog.at_level(logging.DEBUG): + await client.sync_timeout_height() + + expected_log_message_prefix = "error while fetching latest block, setting timeout height to 0: " + found_log = next( + (record for record in caplog.record_tuples if record[2].startswith(expected_log_message_prefix)), + None, + ) + assert (found_log is not None) + assert (found_log[0] == "pyinjective.async_client.AsyncClient") + assert (found_log[1] == logging.DEBUG) + + def test_set_cookie_logs_chain_session_cookie_saved(self, caplog): + caplog.set_level(logging.INFO) + + client = AsyncClient( + network=Network.local(), + insecure=False, + ) + + client.set_cookie(metadata=[("set-cookie", "test-value")], type="chain") + + expected_log_message_prefix = "chain session cookie saved to disk" + found_log = next( + (record for record in caplog.record_tuples if record[2].startswith(expected_log_message_prefix)), + None, + ) + assert (found_log is not None) + assert (found_log[0] == "pyinjective.async_client.AsyncClient") + assert (found_log[1] == logging.INFO) + + @pytest.mark.asyncio + async def test_get_account_logs_exception(self, caplog): + client = AsyncClient( + network=Network.local(), + insecure=False, + ) + + with caplog.at_level(logging.DEBUG): + await client.get_account(address="") + + expected_log_message_prefix = "error while fetching sequence and number " + found_log = next( + (record for record in caplog.record_tuples if record[2].startswith(expected_log_message_prefix)), + None, + ) + assert (found_log is not None) + assert (found_log[0] == "pyinjective.async_client.AsyncClient") + assert (found_log[1] == logging.DEBUG) + + @pytest.mark.asyncio + async def test_initialize_tokens_and_markets( + self, + spot_servicer, + derivative_servicer, + inj_usdt_spot_market_meta, + ape_usdt_spot_market_meta, + btc_usdt_perp_market_meta, + first_match_bet_market_meta, + ): + spot_servicer.markets_queue.append(injective_spot_exchange_rpc_pb2.MarketsResponse( + markets=[inj_usdt_spot_market_meta, ape_usdt_spot_market_meta] + )) + derivative_servicer.markets_queue.append(injective_derivative_exchange_rpc_pb2.MarketsResponse( + markets=[btc_usdt_perp_market_meta] + )) + derivative_servicer.binary_option_markets_queue.append( + injective_derivative_exchange_rpc_pb2.BinaryOptionsMarketsResponse( + markets=[first_match_bet_market_meta] + )) + + client = AsyncClient( + network=Network.local(), + insecure=False, + ) + + client.stubSpotExchange = spot_servicer + client.stubDerivativeExchange = derivative_servicer + + await client._initialize_tokens_and_markets() + + all_tokens = await client.all_tokens() + assert(5 == len(all_tokens)) + inj_symbol, usdt_symbol = inj_usdt_spot_market_meta.ticker.split("/") + ape_symbol, _ = ape_usdt_spot_market_meta.ticker.split("/") + alternative_usdt_name = ape_usdt_spot_market_meta.quote_token_meta.name + usdt_perp_symbol = btc_usdt_perp_market_meta.quote_token_meta.symbol + assert(inj_symbol in all_tokens) + assert(usdt_symbol in all_tokens) + assert(alternative_usdt_name in all_tokens) + assert(ape_symbol in all_tokens) + assert(usdt_perp_symbol in all_tokens) + + all_spot_markets = await client.all_spot_markets() + assert (2 == len(all_spot_markets)) + assert (any((inj_usdt_spot_market_meta.market_id == market.id for market in all_spot_markets.values()))) + assert (any((ape_usdt_spot_market_meta.market_id == market.id for market in all_spot_markets.values()))) + + all_derivative_markets = await client.all_derivative_markets() + assert (1 == len(all_derivative_markets)) + assert (any((btc_usdt_perp_market_meta.market_id == market.id for market in all_derivative_markets.values()))) + + all_binary_option_markets = await client.all_binary_option_markets() + assert (1 == len(all_binary_option_markets)) + assert (any((first_match_bet_market_meta.market_id == market.id for market in all_binary_option_markets.values()))) diff --git a/tests/test_composer.py b/tests/test_composer.py new file mode 100644 index 00000000..9c5426e5 --- /dev/null +++ b/tests/test_composer.py @@ -0,0 +1,235 @@ +import logging +import pytest +from decimal import Decimal + +from pyinjective.composer import Composer +from pyinjective.constant import Denom, Network +from pyinjective.core.market import BinaryOptionMarket, DerivativeMarket, SpotMarket +from pyinjective.proto.injective.exchange.v1beta1 import exchange_pb2 +from tests.model_fixtures.markets_fixtures import ( + btc_usdt_perp_market, + first_match_bet_market, + inj_token, + inj_usdt_spot_market, + usdt_token, + usdt_perp_token +) + + +class TestComposer: + + @pytest.fixture + def inj_usdt_market_id(self): + return "0xa508cb32923323679f29a032c70342c147c17d0145625922b0ef22e955c844c0" + + @pytest.fixture + def basic_composer(self, inj_usdt_spot_market, btc_usdt_perp_market, first_match_bet_market): + composer = Composer( + network=Network.devnet().string(), + spot_markets={inj_usdt_spot_market.id: inj_usdt_spot_market}, + derivative_markets={btc_usdt_perp_market.id: btc_usdt_perp_market}, + binary_option_markets={first_match_bet_market.id: first_match_bet_market}, + tokens={ + inj_usdt_spot_market.base_token.symbol: inj_usdt_spot_market.base_token, + inj_usdt_spot_market.quote_token.symbol: inj_usdt_spot_market.quote_token, + btc_usdt_perp_market.quote_token.symbol: btc_usdt_perp_market.quote_token, + } + ) + + return composer + + def test_composer_initialization_from_ini_files(self): + composer = Composer(network=Network.devnet().string()) + + inj_token = composer.tokens["INJ"] + inj_usdt_spot_market = next((market for market in composer.spot_markets.values() if market.ticker == "'Devnet Spot INJ/USDT'")) + inj_usdt_perp_market = next((market for market in composer.derivative_markets.values() if market.ticker == "'Devnet Derivative INJ/USDT PERP'")) + + assert (18 == inj_token.decimals) + assert (18 == inj_usdt_spot_market.base_token.decimals) + assert (6 == inj_usdt_spot_market.quote_token.decimals) + assert (6 == inj_usdt_perp_market.quote_token.decimals) + + + def test_buy_spot_order_creation(self, basic_composer: Composer, inj_usdt_spot_market: SpotMarket): + fee_recipient = "inj1hkhdaj2a2clmq5jq6mspsggqs32vynpk228q3r" + price = 6.869 + quantity = 1587 + order = basic_composer.SpotOrder( + market_id=inj_usdt_spot_market.id, + subaccount_id="1", + fee_recipient=fee_recipient, + price=price, + quantity=quantity, + is_buy=True, + ) + + price_decimals = inj_usdt_spot_market.quote_token.decimals - inj_usdt_spot_market.base_token.decimals + chain_format_price = Decimal(str(price)) * Decimal(f"1e{price_decimals}") + expected_price = ((chain_format_price // inj_usdt_spot_market.min_price_tick_size) + * inj_usdt_spot_market.min_price_tick_size + * Decimal(f"1e18")) + chain_format_quantity = Decimal(str(quantity)) * Decimal(f"1e{inj_usdt_spot_market.base_token.decimals}") + expected_quantity = ((chain_format_quantity // inj_usdt_spot_market.min_quantity_tick_size) + * inj_usdt_spot_market.min_quantity_tick_size + * Decimal(f"1e18")) + + assert (order.market_id == inj_usdt_spot_market.id) + assert (order.order_info.subaccount_id == "1") + assert (order.order_info.fee_recipient == fee_recipient) + assert (order.order_info.price == str(int(expected_price))) + assert (order.order_info.quantity == str(int(expected_quantity))) + assert (order.order_type == exchange_pb2.OrderType.BUY) + assert (order.trigger_price == "0") + + def test_buy_derivative_order_creation(self, basic_composer: Composer, btc_usdt_perp_market: DerivativeMarket): + fee_recipient = "inj1hkhdaj2a2clmq5jq6mspsggqs32vynpk228q3r" + price = 6.869 + quantity = 1587 + leverage = 2 + order = basic_composer.DerivativeOrder( + market_id=btc_usdt_perp_market.id, + subaccount_id="1", + fee_recipient=fee_recipient, + price=price, + quantity=quantity, + is_buy=True, + leverage=leverage, + ) + + price_decimals = btc_usdt_perp_market.quote_token.decimals + chain_format_price = Decimal(str(price)) * Decimal(f"1e{price_decimals}") + expected_price = ((chain_format_price // btc_usdt_perp_market.min_price_tick_size) + * btc_usdt_perp_market.min_price_tick_size + * Decimal(f"1e18")) + chain_format_quantity = Decimal(str(quantity)) + expected_quantity = ((chain_format_quantity // btc_usdt_perp_market.min_quantity_tick_size) + * btc_usdt_perp_market.min_quantity_tick_size + * Decimal(f"1e18")) + chain_format_margin = (chain_format_quantity * chain_format_price) / Decimal(leverage) + expected_margin = ((chain_format_margin // btc_usdt_perp_market.min_quantity_tick_size) + * btc_usdt_perp_market.min_quantity_tick_size + * Decimal(f"1e18")) + + assert (order.market_id == btc_usdt_perp_market.id) + assert (order.order_info.subaccount_id == "1") + assert (order.order_info.fee_recipient == fee_recipient) + assert (order.order_info.price == str(int(expected_price))) + assert (order.order_info.quantity == str(int(expected_quantity))) + assert (order.order_type == exchange_pb2.OrderType.BUY) + assert (order.margin == str(int(expected_margin))) + assert (order.trigger_price == "0") + + def test_increase_position_margin(self, basic_composer: Composer, btc_usdt_perp_market: DerivativeMarket): + sender = "inj1hkhdaj2a2clmq5jq6mspsggqs32vynpk228q3r" + amount = 1587.789 + message = basic_composer.MsgIncreasePositionMargin( + sender=sender, + source_subaccount_id="1", + destination_subaccount_id="2", + market_id=btc_usdt_perp_market.id, + amount=amount, + ) + + price_decimals = btc_usdt_perp_market.quote_token.decimals + chain_format_margin = Decimal(str(amount)) * Decimal(f"1e{price_decimals}") + expected_margin = ((chain_format_margin // btc_usdt_perp_market.min_quantity_tick_size) + * btc_usdt_perp_market.min_quantity_tick_size + * Decimal(f"1e18")) + + assert (message.market_id == btc_usdt_perp_market.id) + assert (message.sender == sender) + assert (message.source_subaccount_id == "1") + assert (message.destination_subaccount_id == "2") + assert (message.amount == str(int(expected_margin))) + + def test_buy_binary_option_order_creation_with_fixed_denom( + self, + basic_composer: Composer, + first_match_bet_market: BinaryOptionMarket + ): + fee_recipient = "inj1hkhdaj2a2clmq5jq6mspsggqs32vynpk228q3r" + price = 6.869 + quantity = 1587 + fixed_denom = Denom( + description="Fixed denom", + base=2, + quote=6, + min_price_tick_size=1000, + min_quantity_tick_size=10000, + ) + + order = basic_composer.BinaryOptionsOrder( + market_id=first_match_bet_market.id, + subaccount_id="1", + fee_recipient=fee_recipient, + price=price, + quantity=quantity, + is_buy=True, + denom=fixed_denom, + ) + + price_decimals = fixed_denom.quote + chain_format_price = Decimal(str(price)) * Decimal(f"1e{price_decimals}") + expected_price = ((chain_format_price // Decimal(str(fixed_denom.min_price_tick_size))) + * Decimal(str(fixed_denom.min_price_tick_size)) + * Decimal(f"1e18")) + quantity_decimals = fixed_denom.base + chain_format_quantity = Decimal(str(quantity)) * Decimal(f"1e{quantity_decimals}") + expected_quantity = ((chain_format_quantity // Decimal(str(fixed_denom.min_quantity_tick_size))) + * Decimal(str(fixed_denom.min_quantity_tick_size)) + * Decimal(f"1e18")) + chain_format_margin = chain_format_quantity * chain_format_price + expected_margin = ((chain_format_margin // Decimal(str(fixed_denom.min_quantity_tick_size))) + * Decimal(str(fixed_denom.min_quantity_tick_size)) + * Decimal(f"1e18")) + + assert (order.market_id == first_match_bet_market.id) + assert (order.order_info.subaccount_id == "1") + assert (order.order_info.fee_recipient == fee_recipient) + assert (order.order_info.price == str(int(expected_price))) + assert (order.order_info.quantity == str(int(expected_quantity))) + assert (order.order_type == exchange_pb2.OrderType.BUY) + assert (order.margin == str(int(expected_margin))) + assert (order.trigger_price == "0") + + def test_buy_binary_option_order_creation_without_fixed_denom( + self, + basic_composer: Composer, + first_match_bet_market: BinaryOptionMarket, + ): + fee_recipient = "inj1hkhdaj2a2clmq5jq6mspsggqs32vynpk228q3r" + price = 6.869 + quantity = 1587 + + order = basic_composer.BinaryOptionsOrder( + market_id=first_match_bet_market.id, + subaccount_id="1", + fee_recipient=fee_recipient, + price=price, + quantity=quantity, + is_buy=True, + ) + + price_decimals = first_match_bet_market.quote_token.decimals + chain_format_price = Decimal(str(price)) * Decimal(f"1e{price_decimals}") + expected_price = ((chain_format_price // first_match_bet_market.min_price_tick_size) + * first_match_bet_market.min_price_tick_size + * Decimal(f"1e18")) + chain_format_quantity = Decimal(str(quantity)) + expected_quantity = ((chain_format_quantity // first_match_bet_market.min_quantity_tick_size) + * first_match_bet_market.min_quantity_tick_size + * Decimal(f"1e18")) + chain_format_margin = (chain_format_quantity * chain_format_price) + expected_margin = ((chain_format_margin // first_match_bet_market.min_quantity_tick_size) + * first_match_bet_market.min_quantity_tick_size + * Decimal(f"1e18")) + + assert (order.market_id == first_match_bet_market.id) + assert (order.order_info.subaccount_id == "1") + assert (order.order_info.fee_recipient == fee_recipient) + assert (order.order_info.price == str(int(expected_price))) + assert (order.order_info.quantity == str(int(expected_quantity))) + assert (order.order_type == exchange_pb2.OrderType.BUY) + assert (order.margin == str(int(expected_margin))) + assert (order.trigger_price == "0")