diff --git a/Pipfile.lock b/Pipfile.lock index 822d20a..46b2f91 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -96,22 +96,24 @@ }, "pynacl": { "hashes": [ - "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4", + "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420", "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4", - "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574", - "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d", - "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25", - "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f", + "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6", + "sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6", + "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7", "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505", "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122", - "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7", - "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420", - "sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f", - "sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96", - "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6", + "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4", + "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f", "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514", "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff", - "sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80" + "sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96", + "sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80", + "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25", + "sha256:4e10569f8cbed81cb7526ae137049759d2a8d57726d52c1a000a3ce366779634", + "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d", + "sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f", + "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.4.0" @@ -134,11 +136,11 @@ }, "solana": { "hashes": [ - "sha256:0ccb0696272ed91d9eff92a8b4840816d8a9383c3d7665eda591386560c54f70", - "sha256:f607d4f66382bf37a76efed5b55e119842b65def23d1c5fe441619d04170e2df" + "sha256:0bb866b3a046ad41f06ad9ad94b7ee9f7e2209c20c038c240f15b17ffd6c97bc", + "sha256:f9dd7391e628e1bf7c611cf4445936c84f03ec7551db7f400ef68a5fc2c8cfea" ], "index": "pypi", - "version": "==0.2.0" + "version": "==0.3.1" }, "typing-extensions": { "hashes": [ @@ -227,19 +229,18 @@ }, "black": { "hashes": [ - "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea", - "sha256:70b62ef1527c950db59062cda342ea224d772abdf6adc58b86a45421bab20a6b" + "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea" ], "index": "pypi", "version": "==20.8b1" }, "bleach": { "hashes": [ - "sha256:2bce3d8fab545a6528c8fa5d9f9ae8ebc85a56da365c7f85180bfe96a35ef22f", - "sha256:3c4c520fdb9db59ef139915a5db79f8b51bc2a7257ea0389f30c846883430a4b" + "sha256:769483204d247465c0b001ead257fb86bba6944bce6fe1b6759c812cceb54e3d", + "sha256:f9e0205cc57b558c21bdfc11034f9d96b14c4052c25be60885d94f4277c792e0" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==3.1.5" + "version": "==3.2.0" }, "certifi": { "hashes": [ @@ -429,11 +430,11 @@ }, "jupyterlab": { "hashes": [ - "sha256:a0a1882456098d2fab4c241a0b16a1df96c36de1c45bddbf5fc40867e3d9340e", - "sha256:a72ffd0d919cba03a5ef8422bc92c3332a957ff97b0490494209c83ad93826da" + "sha256:95d0509557881cfa8a5fcdf225f2fca46faf1bc52fc56a28e0b72fcc594c90ab", + "sha256:c8377bee30504919c1e79949f9fe35443ab7f5c4be622c95307e8108410c8b8c" ], "index": "pypi", - "version": "==2.2.7" + "version": "==2.2.8" }, "jupyterlab-pygments": { "hashes": [ @@ -575,11 +576,11 @@ }, "nbconvert": { "hashes": [ - "sha256:970122eaf3a3ddcfe4e03514b219df4be4af09e70c748faf6ba96f51a25fd09b", - "sha256:db94117fbac29153834447e31b30cda337d4450e46e0bdb1a36eafbbf4435156" + "sha256:06c64fd45d4b6424e88eb3bf7e5eb205a0fc8a4c0a69666f0b9a2262c76f59e1", + "sha256:d8490f40368a1324521f8e740a0e341dc40bcd6e6926da64fa64b3a8801f16a3" ], "markers": "python_version >= '3.6'", - "version": "==6.0.1" + "version": "==6.0.3" }, "nbformat": { "hashes": [ @@ -722,11 +723,11 @@ }, "pygments": { "hashes": [ - "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44", - "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324" + "sha256:2594e8fdb06fef91552f86f4fd3a244d148ab24b66042036e64f29a291515048", + "sha256:2df50d16b45b977217e02cba6c8422aaddb859f3d0570a88e09b00eafae89c6e" ], "markers": "python_version >= '3.5'", - "version": "==2.6.1" + "version": "==2.7.0" }, "pylint": { "hashes": [ @@ -746,17 +747,18 @@ }, "pyrsistent": { "hashes": [ - "sha256:28669905fe725965daa16184933676547c5bb40a5153055a8dee2a4bd7933ad3" + "sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e" ], - "version": "==0.16.0" + "markers": "python_version >= '3.5'", + "version": "==0.17.3" }, "pytest": { "hashes": [ - "sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4", - "sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad" + "sha256:0e37f61339c4578776e090c3b8f6b16ce4db333889d65d0efb305243ec544b40", + "sha256:c8f57c2a30983f469bf03e68cdfa74dc474ce56b8f280ddcb080dfd91df01043" ], "index": "pypi", - "version": "==6.0.1" + "version": "==6.0.2" }, "pytest-tornasync": { "hashes": [ diff --git a/src/client.py b/src/client.py new file mode 100644 index 0000000..e9b9b80 --- /dev/null +++ b/src/client.py @@ -0,0 +1,6 @@ +from solana.rpc.api import Client + + +def market_client(endpoint: str) -> Client: + """RPC client to interact with the Serum Dex.""" + return Client(endpoint) diff --git a/src/instructions.py b/src/instructions.py index cc265b1..091fb4f 100644 --- a/src/instructions.py +++ b/src/instructions.py @@ -5,12 +5,12 @@ from solana.publickey import PublicKey from solana.sysvar import SYSVAR_RENT_PUBKEY from solana.transaction import AccountMeta, TransactionInstruction from solana.utils.validate import validate_instruction_keys, validate_instruction_type +from spl.token.constants import TOKEN_PROGRAM_ID # type: ignore # TODO: Fix and remove ignore. from ._layouts.instructions import INSTRUCTIONS_LAYOUT, InstructionType from .enums import OrderType, Side DEFAULT_DEX_PROGRAM_ID = PublicKey("4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn") -TOKEN_PROGRAM_ID = PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") class InitializeMarketParams(NamedTuple): diff --git a/src/market.py b/src/market.py index e578a7a..1d66aed 100644 --- a/src/market.py +++ b/src/market.py @@ -5,6 +5,7 @@ import logging import math from typing import Any, Iterable, List, NamedTuple +from construct import Struct as cStruct # type: ignore from solana.account import Account from solana.publickey import PublicKey from solana.rpc.api import Client @@ -19,7 +20,7 @@ from .instructions import DEFAULT_DEX_PROGRAM_ID, CancelOrderParams, MatchOrders from .instructions import cancel_order as cancel_order_inst from .instructions import match_orders as match_order_inst from .instructions import new_order as new_order_inst -from .open_order_account import OpenOrderAccount, make_create_account_instruction +from .open_orders_account import OpenOrdersAccount, make_create_account_instruction from .queue_ import decode_event_queue, decode_request_queue from .utils import load_bytes_data @@ -40,11 +41,11 @@ class Market: # pylint: disable=too-many-arguments def __init__( self, - decoded: Any, + decoded: Any, # Construct structure of the market. base_mint_decimals: int, quote_mint_decimals: int, options: Any, # pylint: disable=unused-argument - endpoint: str, + conn: Client, program_id: PublicKey = DEFAULT_DEX_PROGRAM_ID, ) -> None: # TODO: add options @@ -56,25 +57,28 @@ class Market: self._skip_preflight = False self._confirmations = 10 self._program_id = program_id - self._endpoint = endpoint + self._conn = conn + + @staticmethod + def LAYOUT() -> cStruct: # pylint: disable=invalid-name + """Construct layout of the market state.""" + return MARKET_LAYOUT @staticmethod # pylint: disable=unused-argument - def load( - endpoint: str, market_address: str, options: Any, program_id: PublicKey = DEFAULT_DEX_PROGRAM_ID - ) -> Market: + def load(conn: Client, market_address: str, options: Any, program_id: PublicKey = DEFAULT_DEX_PROGRAM_ID) -> Market: """Factory method to create a Market.""" - bytes_data = load_bytes_data(PublicKey(market_address), endpoint) + bytes_data = load_bytes_data(PublicKey(market_address), conn) market_state = MARKET_LAYOUT.parse(bytes_data) # TODO: add ownAddress check! if not market_state.account_flags.initialized or not market_state.account_flags.market: raise Exception("Invalid market") - base_mint_decimals = Market.get_mint_decimals(endpoint, PublicKey(market_state.base_mint)) - quote_mint_decimals = Market.get_mint_decimals(endpoint, PublicKey(market_state.quote_mint)) + base_mint_decimals = Market.get_mint_decimals(conn, PublicKey(market_state.base_mint)) + quote_mint_decimals = Market.get_mint_decimals(conn, PublicKey(market_state.quote_mint)) - return Market(market_state, base_mint_decimals, quote_mint_decimals, options, endpoint, program_id=program_id) + return Market(market_state, base_mint_decimals, quote_mint_decimals, options, conn, program_id=program_id) def address(self) -> PublicKey: """Return market address.""" @@ -107,7 +111,7 @@ class Market: return PublicKey(self._decode.request_queue) def event_queue(self) -> PublicKey: - """Returns quote vault address.""" + """Returns event queue address.""" return PublicKey(self._decode.event_queue) def __base_spl_token_multiplier(self) -> int: @@ -142,36 +146,54 @@ class Market: return int(math.floor(size * 10 ** self._base_spl_token_decimals) / self._decode.base_lot_size) @staticmethod - def get_mint_decimals(endpoint: str, mint_pub_key: PublicKey) -> int: + def get_mint_decimals(conn: Client, mint_pub_key: PublicKey) -> int: """Get the mint decimals from given public key.""" - bytes_data = load_bytes_data(mint_pub_key, endpoint) + bytes_data = load_bytes_data(mint_pub_key, conn) return MINT_LAYOUT.parse(bytes_data).decimals + def bids_address(self) -> PublicKey: + return PublicKey(self._decode.bids) + + def asks_address(self) -> PublicKey: + return PublicKey(self._decode.asks) + + def find_open_orders_accounts_for_owner(self, owner_address: PublicKey) -> List[OpenOrdersAccount]: + return OpenOrdersAccount.find_for_market_and_owner(self._conn, self.address(), owner_address, self._program_id) + + def find_quote_token_accounts_for_owner(self, owner_address: PublicKey, include_unwrapped_sol: bool = False): + raise NotImplementedError("find_quote_token_accounts_for_owner not implemented.") + def load_bids(self) -> OrderBook: """Load the bid order book""" bids_addr = PublicKey(self._decode.bids) - bytes_data = load_bytes_data(bids_addr, self._endpoint) + bytes_data = load_bytes_data(bids_addr, self._conn) return OrderBook.decode(self, bytes_data) def load_asks(self) -> OrderBook: """Load the Ask order book.""" asks_addr = PublicKey(self._decode.asks) - bytes_data = load_bytes_data(asks_addr, self._endpoint) + bytes_data = load_bytes_data(asks_addr, self._conn) return OrderBook.decode(self, bytes_data) + def load_orders_for_owner(self) -> List[Order]: + raise NotImplementedError("load_orders_for_owner not implemented.") + + def load_base_token_for_owner(self): + raise NotImplementedError("load_base_token_for_owner not implemented.") + def load_event_queue(self): # returns raw construct type event_queue_addr = PublicKey(self._decode.event_queue) - bytes_data = load_bytes_data(event_queue_addr, self._endpoint) + bytes_data = load_bytes_data(event_queue_addr, self._conn) return decode_event_queue(bytes_data) def load_request_queue(self): # returns raw construct type request_queue_addr = PublicKey(self._decode.request_queue) - bytes_data = load_bytes_data(request_queue_addr, self._endpoint) + bytes_data = load_bytes_data(request_queue_addr, self._conn) return decode_request_queue(bytes_data) def load_fills(self, limit=100) -> List[FilledOrder]: event_queue_addr = PublicKey(self._decode.event_queue) - bytes_data = load_bytes_data(event_queue_addr, self._endpoint) + bytes_data = load_bytes_data(event_queue_addr, self._conn) events = decode_event_queue(bytes_data, limit) return [ self.parse_fill_event(event) @@ -216,24 +238,27 @@ class Market: limit_price: int, max_quantity: int, client_id: int = 0, - ): + ): # TODO: Add open_orders_address_key param transaction = Transaction() signers: List[Account] = [owner] open_order_accounts = self.find_open_orders_accounts_for_owner(owner.public_key()) if not open_order_accounts: new_open_order_account = Account() + mbfre_resp = self._conn.get_minimum_balance_for_rent_exemption(OPEN_ORDERS_LAYOUT.sizeof()) + balanced_needed = mbfre_resp["result"] transaction.add( make_create_account_instruction( owner.public_key(), new_open_order_account.public_key(), - Client(self._endpoint).get_minimum_balance_for_rent_exemption(OPEN_ORDERS_LAYOUT.sizeof())[ - "result" - ], + balanced_needed, self._program_id, ) ) signers.append(new_open_order_account) + # TODO: Handle open_orders_address_key + # TODO: Handle wrapped sol account + transaction.add( self.make_place_order_instruction( payer, @@ -281,23 +306,16 @@ class Market: ) ) - def find_open_orders_accounts_for_owner(self, owner_address: PublicKey) -> List[OpenOrderAccount]: - return OpenOrderAccount.find_for_market_and_owner( - self._endpoint, self.address(), owner_address, self._program_id - ) - def cancel_order_by_client_id(self, owner: str) -> str: - pass + raise NotImplementedError("cancel_order_by_client_id not implemented.") def cancel_order(self, owner: Account, order: Order) -> str: - transaction = Transaction() - transaction.add(self.make_cancel_order_instruction(owner.public_key(), order)) - return self._send_transaction(transaction, owner) + txn = Transaction().add(self.make_cancel_order_instruction(owner.public_key(), order)) + return self._send_transaction(txn, owner) def match_orders(self, fee_payer: Account, limit: int) -> str: - transaction = Transaction() - transaction.add(self.make_match_orders_instruction(limit)) - return self._send_transaction(transaction, fee_payer) + txn = Transaction().add(self.make_match_orders_instruction(limit)) + return self._send_transaction(txn, fee_payer) def make_cancel_order_instruction(self, owner: PublicKey, order: Order) -> TransactionInstruction: params = CancelOrderParams( @@ -326,9 +344,13 @@ class Market: ) return match_order_inst(params) + def settle_funds( + self, owner: Account, open_orders: OpenOrdersAccount, base_wallet: PublicKey, quote_wallet: PublicKey + ) -> str: + raise NotImplementedError("settle_funds not implemented.") + def _send_transaction(self, transaction: Transaction, *signers: Account) -> str: - connection = Client(self._endpoint) - res = connection.send_transaction(transaction, *signers, skip_preflight=self._skip_preflight) + res = self._conn.send_transaction(transaction, *signers, skip_preflight=self._skip_preflight) if self._confirmations > 0: self.logger.warning("Cannot confirm transaction yet.") signature = res.get("result") diff --git a/src/open_order_account.py b/src/open_orders_account.py similarity index 88% rename from src/open_order_account.py rename to src/open_orders_account.py index dd960f1..e5c2099 100644 --- a/src/open_order_account.py +++ b/src/open_orders_account.py @@ -21,7 +21,7 @@ class ProgramAccount(NamedTuple): owner: PublicKey -class OpenOrderAccount: +class OpenOrdersAccount: # pylint: disable=too-many-arguments # pylint: disable=too-many-instance-attributes def __init__( @@ -51,12 +51,12 @@ class OpenOrderAccount: self.client_ids = client_ids @staticmethod - def from_bytes(address: PublicKey, data_bytes: bytes) -> OpenOrderAccount: + def from_bytes(address: PublicKey, data_bytes: bytes) -> OpenOrdersAccount: open_order_decoded = OPEN_ORDERS_LAYOUT.parse(data_bytes) if not open_order_decoded.account_flags.open_orders or not open_order_decoded.account_flags.initialized: raise Exception("Not an open order account or not initialized.") - return OpenOrderAccount( + return OpenOrdersAccount( address=address, market=PublicKey(open_order_decoded.market), owner=PublicKey(open_order_decoded.owner), @@ -72,8 +72,8 @@ class OpenOrderAccount: @staticmethod def find_for_market_and_owner( - endpoint: str, market: PublicKey, owner: PublicKey, program_id: PublicKey - ) -> List[OpenOrderAccount]: + conn: Client, market: PublicKey, owner: PublicKey, program_id: PublicKey + ) -> List[OpenOrdersAccount]: filters = [ MemcmpOpt( offset=5 + 8, # 5 bytes of padding, 8 bytes of account flag @@ -85,7 +85,7 @@ class OpenOrderAccount: ), ] - resp = Client(endpoint).get_program_accounts( + resp = conn.get_program_accounts( program_id, encoding="base64", memcmp_opts=filters, data_size=OPEN_ORDERS_LAYOUT.sizeof() ) accounts = [] @@ -100,13 +100,13 @@ class OpenOrderAccount: lamports=int(account_details["lamports"]), ) ) - return [OpenOrderAccount.from_bytes(account.public_key, account.data) for account in accounts] + return [OpenOrdersAccount.from_bytes(account.public_key, account.data) for account in accounts] @staticmethod - def load(endpoint: str, address: str) -> OpenOrderAccount: + def load(conn: Client, address: str) -> OpenOrdersAccount: addr_pub_key = PublicKey(address) - bytes_data = load_bytes_data(addr_pub_key, endpoint) - return OpenOrderAccount.from_bytes(addr_pub_key, bytes_data) + bytes_data = load_bytes_data(addr_pub_key, conn) + return OpenOrdersAccount.from_bytes(addr_pub_key, bytes_data) def make_create_account_instruction( diff --git a/src/utils.py b/src/utils.py index ac7e540..5afb6c3 100644 --- a/src/utils.py +++ b/src/utils.py @@ -4,8 +4,8 @@ from solana.publickey import PublicKey from solana.rpc.api import Client -def load_bytes_data(addr: PublicKey, endpoint: str): - res = Client(endpoint).get_account_info(addr) +def load_bytes_data(addr: PublicKey, conn: Client): + res = conn.get_account_info(addr) if ("result" not in res) or ("value" not in res["result"]) or ("data" not in res["result"]["value"]): raise Exception("Cannot load byte data.") data = res["result"]["value"]["data"][0] diff --git a/tests/conftest.py b/tests/conftest.py index a10bb56..2c38677 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,26 +5,26 @@ from solana.account import Account from solana.publickey import PublicKey from solana.rpc.api import Client -__cached_params = {} +from src.client import market_client @pytest.mark.integration @pytest.fixture(scope="session") def __bs_params() -> Dict[str, str]: - if not __cached_params: - with open("tests/crank.log") as crank_log: - for line in crank_log.readlines(): - if ":" not in line: - continue - key, val = line.strip().replace(",", "").split(": ") - assert key, "key must not be None" - assert val, "val must not be None" - __cached_params[key] = val - return __cached_params + params = {} + with open("tests/crank.log") as crank_log: + for line in crank_log.readlines(): + if ":" not in line: + continue + key, val = line.strip().replace(",", "").split(": ") + assert key, "key must not be None" + assert val, "val must not be None" + params[key] = val + return params -def __bootstrap_account(pubkey: str, secret: str) -> Account: - secret = [int(b) for b in secret[1:-1].split(" ")] +def __bootstrap_account(pubkey: str, secretkey: str) -> Account: + secret = [int(b) for b in secretkey[1:-1].split(" ")] account = Account(secret) assert str(account.public_key()) == pubkey, "account must map to provided public key" return account @@ -146,5 +146,7 @@ def stubbed_ask_account_pk(__bs_params) -> PublicKey: @pytest.fixture(scope="session") def http_client() -> Client: """Solana http client.""" - client = Client() + client = market_client("http://localhost:8899") + if not client.is_connected(): + raise Exception("Could not connect to local node. Please run `make int-tests` to run integration tests.") return client diff --git a/tests/integration/test_market.py b/tests/integration/test_market.py index def669b..4b3a0d7 100644 --- a/tests/integration/test_market.py +++ b/tests/integration/test_market.py @@ -11,9 +11,9 @@ from .utils import confirm_transaction @pytest.mark.integration -@pytest.fixture(scope="session") -def bootstrapped_market(stubbed_market_pk: PublicKey, stubbed_dex_program_pk: PublicKey) -> Market: - return Market.load("http://localhost:8899", str(stubbed_market_pk), None, program_id=stubbed_dex_program_pk) +@pytest.fixture(scope="module") +def bootstrapped_market(http_client: Client, stubbed_market_pk: PublicKey, stubbed_dex_program_pk: PublicKey) -> Market: + return Market.load(http_client, str(stubbed_market_pk), None, stubbed_dex_program_pk) @pytest.mark.integration diff --git a/tests/test_market.py b/tests/test_market.py index bd8ca15..0b66872 100644 --- a/tests/test_market.py +++ b/tests/test_market.py @@ -1,36 +1,54 @@ import base64 from types import SimpleNamespace +import pytest +from solana.rpc.api import Client + from src.market import MARKET_LAYOUT, Market, Order, OrderBook from .binary_file_path import ASK_ORDER_BIN_PATH -MARKET_DATA_HEX = "736572756d030000000000000054f23cbc4d93795ce75ecd8173bcf436923112f7b6b024f3afd9b6789124b9680000000000000000c73690f1d4b87aa9337848369c20a682b37e8dcb33f4237a2a8f8b0abd64bd1cd9c75c9a58645ff02ab0741cd6d1790067957d2266165b767259b358ded270fb2dbc6d44f2ab58dce432b5bb31d98517366d6e24e69c0bf5b926ead9ec658935408ffd71c50000000000000000000000e1a461a046199877c4cd3cbafc61c3dfdb088e737b7193a3d28e72b709421fc4544fec927c000000a4fc4c12000000006400000000000000f422ea23ada9e1a9d100ba8443deb041c231e0f79ee6d07d1c1f7042fe4a1ade3b236ea4ba636227dfa22773f41fa02cc91842c2e9330e2ac0a987dc68b520e8c58676b5751c48e22c3bcc6edda8f75f76b1596b9874bd5714366e32d84e3bc0400501895361982c4be67d03af519ac7fd96a8a79f5b15ec7af79f6b70290bf740420f00000000001027000000000000000000000000000070616464696e67" # noqa: E501 # pylint: disable=line-too-long -DATA = bytes.fromhex(MARKET_DATA_HEX) -MARKET_ENCODE = SimpleNamespace( - **{ - "account_flags": SimpleNamespace( - **{ - "initialized": True, - "market": True, - "bids": False, - } - ), - "vault_signer_nonce": 0, - "base_fees_accrued": 0, - "quote_dust_threshold": 100, - "base_lot_size": 100, - "quote_lot_size": 10, - "fee_rate_bps": 0, - } -) - -BTC_USDC_MARKET = Market(MARKET_ENCODE, 6, 6, None, "http://stubbed_endpoint:123/") +@pytest.fixture(scope="module") +def stubbed_data() -> bytes: + MARKET_DATA_HEX = ( # pylint: disable=invalid-name + "736572756d030000000000000054f23cbc4d93795ce75ecd8173bcf436923112f7b6b024f3afd9b6789124b9680000000000" + "000000c73690f1d4b87aa9337848369c20a682b37e8dcb33f4237a2a8f8b0abd64bd1cd9c75c9a58645ff02ab0741cd6d179" + "0067957d2266165b767259b358ded270fb2dbc6d44f2ab58dce432b5bb31d98517366d6e24e69c0bf5b926ead9ec65893540" + "8ffd71c50000000000000000000000e1a461a046199877c4cd3cbafc61c3dfdb088e737b7193a3d28e72b709421fc4544fec" + "927c000000a4fc4c12000000006400000000000000f422ea23ada9e1a9d100ba8443deb041c231e0f79ee6d07d1c1f7042fe" + "4a1ade3b236ea4ba636227dfa22773f41fa02cc91842c2e9330e2ac0a987dc68b520e8c58676b5751c48e22c3bcc6edda8f7" + "5f76b1596b9874bd5714366e32d84e3bc0400501895361982c4be67d03af519ac7fd96a8a79f5b15ec7af79f6b70290bf740" + "420f00000000001027000000000000000000000000000070616464696e67" + ) + return bytes.fromhex(MARKET_DATA_HEX) -def test_parse_market_state(): - parsed_market = MARKET_LAYOUT.parse(DATA) +@pytest.fixture(scope="module") +def stubbed_market() -> Market: + conn = Client("http://stubbed_endpoint:123/") + MARKET_ENCODE = SimpleNamespace( # pylint: disable=invalid-name + **{ + "account_flags": SimpleNamespace( + **{ + "initialized": True, + "market": True, + "bids": False, + } + ), + "vault_signer_nonce": 0, + "base_fees_accrued": 0, + "quote_dust_threshold": 100, + "base_lot_size": 100, + "quote_lot_size": 10, + "fee_rate_bps": 0, + } + ) + return Market(MARKET_ENCODE, 6, 6, None, conn) + + +def test_parse_market_state(stubbed_data): # pylint: disable=redefined-outer-name + parsed_market = MARKET_LAYOUT.parse(stubbed_data) assert parsed_market.account_flags.initialized assert parsed_market.account_flags.market assert not parsed_market.account_flags.open_orders @@ -40,31 +58,31 @@ def test_parse_market_state(): assert parsed_market.fee_rate_bps == 0 -def test_order_book_iterator(): +def test_order_book_iterator(stubbed_market): # pylint: disable=redefined-outer-name """Test order book parsing.""" with open(ASK_ORDER_BIN_PATH, "r") as input_file: base64_res = input_file.read() data = base64.decodebytes(base64_res.encode("ascii")) - order_book = OrderBook.decode(BTC_USDC_MARKET, data) + order_book = OrderBook.decode(stubbed_market, data) total_orders = sum([1 for _ in order_book.orders()]) assert total_orders == 15 -def test_order_book_get_l2(): +def test_order_book_get_l2(stubbed_market): # pylint: disable=redefined-outer-name with open(ASK_ORDER_BIN_PATH, "r") as input_file: base64_res = input_file.read() data = base64.decodebytes(base64_res.encode("ascii")) - order_book = OrderBook.decode(BTC_USDC_MARKET, data) + order_book = OrderBook.decode(stubbed_market, data) for i in range(1, 16): assert i == len(order_book.get_l2(i)) assert [(11744.6, 4.0632, 117446, 40632)] == order_book.get_l2(1) -def test_order_book_iterable(): +def test_order_book_iterable(stubbed_market): # pylint: disable=redefined-outer-name with open(ASK_ORDER_BIN_PATH, "r") as input_file: base64_res = input_file.read() data = base64.decodebytes(base64_res.encode("ascii")) - order_book = OrderBook.decode(BTC_USDC_MARKET, data) + order_book = OrderBook.decode(stubbed_market, data) cnt = 0 for order in order_book: cnt += 1 diff --git a/tests/test_open_order_account.py b/tests/test_open_orders_account.py similarity index 91% rename from tests/test_open_order_account.py rename to tests/test_open_orders_account.py index 446b121..56f4593 100644 --- a/tests/test_open_order_account.py +++ b/tests/test_open_orders_account.py @@ -2,7 +2,7 @@ import base64 from solana.publickey import PublicKey -from src.open_order_account import OPEN_ORDERS_LAYOUT, OpenOrderAccount +from src.open_orders_account import OPEN_ORDERS_LAYOUT, OpenOrdersAccount from .binary_file_path import OPEN_ORDER_ACCOUNT_BIN_PATH @@ -28,7 +28,7 @@ def test_decode_open_order_account(): with open(OPEN_ORDER_ACCOUNT_BIN_PATH, "r") as input_file: base64_res = input_file.read() data = base64.decodebytes(base64_res.encode("ascii")) - open_order_account = OpenOrderAccount.from_bytes(PublicKey(1), data) + open_order_account = OpenOrdersAccount.from_bytes(PublicKey(1), data) assert open_order_account.market == PublicKey("4r5Bw3HxmxAzPQ2ATUvgF2nFe3B6G1Z2Nq2Nwu77wWc2") assert open_order_account.owner == PublicKey("7hJx7QMiVfjZSSADQ18oNKzqifJPMu18djYLkh4aYh5Q") assert len([order for order in open_order_account.orders if order != 0]) == 3