Replace raw endpoints with Client and scaffold remaining market functions (#34)

* Bump solana to 0.3.1

* Use client instead of raw endpoint

* Use TOKEN_PROGRAM_ID from spl.constants

* Scaffold unimplemented functions

* Rename open_order to open_orders

* Remove unnecessary code

* Remove unused import

* Add market client
This commit is contained in:
Michael Huang 2020-09-17 00:11:39 -05:00 committed by GitHub
parent e2351f5134
commit 9abb0ca3a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 181 additions and 131 deletions

68
Pipfile.lock generated
View File

@ -96,22 +96,24 @@
}, },
"pynacl": { "pynacl": {
"hashes": [ "hashes": [
"sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4", "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420",
"sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4", "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4",
"sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574", "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6",
"sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d", "sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6",
"sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25", "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7",
"sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f",
"sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505", "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505",
"sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122", "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122",
"sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7", "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4",
"sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420", "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f",
"sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f",
"sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96",
"sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6",
"sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514", "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514",
"sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff", "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'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.4.0" "version": "==1.4.0"
@ -134,11 +136,11 @@
}, },
"solana": { "solana": {
"hashes": [ "hashes": [
"sha256:0ccb0696272ed91d9eff92a8b4840816d8a9383c3d7665eda591386560c54f70", "sha256:0bb866b3a046ad41f06ad9ad94b7ee9f7e2209c20c038c240f15b17ffd6c97bc",
"sha256:f607d4f66382bf37a76efed5b55e119842b65def23d1c5fe441619d04170e2df" "sha256:f9dd7391e628e1bf7c611cf4445936c84f03ec7551db7f400ef68a5fc2c8cfea"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.2.0" "version": "==0.3.1"
}, },
"typing-extensions": { "typing-extensions": {
"hashes": [ "hashes": [
@ -227,19 +229,18 @@
}, },
"black": { "black": {
"hashes": [ "hashes": [
"sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea", "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"
"sha256:70b62ef1527c950db59062cda342ea224d772abdf6adc58b86a45421bab20a6b"
], ],
"index": "pypi", "index": "pypi",
"version": "==20.8b1" "version": "==20.8b1"
}, },
"bleach": { "bleach": {
"hashes": [ "hashes": [
"sha256:2bce3d8fab545a6528c8fa5d9f9ae8ebc85a56da365c7f85180bfe96a35ef22f", "sha256:769483204d247465c0b001ead257fb86bba6944bce6fe1b6759c812cceb54e3d",
"sha256:3c4c520fdb9db59ef139915a5db79f8b51bc2a7257ea0389f30c846883430a4b" "sha256:f9e0205cc57b558c21bdfc11034f9d96b14c4052c25be60885d94f4277c792e0"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "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": { "certifi": {
"hashes": [ "hashes": [
@ -429,11 +430,11 @@
}, },
"jupyterlab": { "jupyterlab": {
"hashes": [ "hashes": [
"sha256:a0a1882456098d2fab4c241a0b16a1df96c36de1c45bddbf5fc40867e3d9340e", "sha256:95d0509557881cfa8a5fcdf225f2fca46faf1bc52fc56a28e0b72fcc594c90ab",
"sha256:a72ffd0d919cba03a5ef8422bc92c3332a957ff97b0490494209c83ad93826da" "sha256:c8377bee30504919c1e79949f9fe35443ab7f5c4be622c95307e8108410c8b8c"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.2.7" "version": "==2.2.8"
}, },
"jupyterlab-pygments": { "jupyterlab-pygments": {
"hashes": [ "hashes": [
@ -575,11 +576,11 @@
}, },
"nbconvert": { "nbconvert": {
"hashes": [ "hashes": [
"sha256:970122eaf3a3ddcfe4e03514b219df4be4af09e70c748faf6ba96f51a25fd09b", "sha256:06c64fd45d4b6424e88eb3bf7e5eb205a0fc8a4c0a69666f0b9a2262c76f59e1",
"sha256:db94117fbac29153834447e31b30cda337d4450e46e0bdb1a36eafbbf4435156" "sha256:d8490f40368a1324521f8e740a0e341dc40bcd6e6926da64fa64b3a8801f16a3"
], ],
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==6.0.1" "version": "==6.0.3"
}, },
"nbformat": { "nbformat": {
"hashes": [ "hashes": [
@ -722,11 +723,11 @@
}, },
"pygments": { "pygments": {
"hashes": [ "hashes": [
"sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44", "sha256:2594e8fdb06fef91552f86f4fd3a244d148ab24b66042036e64f29a291515048",
"sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324" "sha256:2df50d16b45b977217e02cba6c8422aaddb859f3d0570a88e09b00eafae89c6e"
], ],
"markers": "python_version >= '3.5'", "markers": "python_version >= '3.5'",
"version": "==2.6.1" "version": "==2.7.0"
}, },
"pylint": { "pylint": {
"hashes": [ "hashes": [
@ -746,17 +747,18 @@
}, },
"pyrsistent": { "pyrsistent": {
"hashes": [ "hashes": [
"sha256:28669905fe725965daa16184933676547c5bb40a5153055a8dee2a4bd7933ad3" "sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"
], ],
"version": "==0.16.0" "markers": "python_version >= '3.5'",
"version": "==0.17.3"
}, },
"pytest": { "pytest": {
"hashes": [ "hashes": [
"sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4", "sha256:0e37f61339c4578776e090c3b8f6b16ce4db333889d65d0efb305243ec544b40",
"sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad" "sha256:c8f57c2a30983f469bf03e68cdfa74dc474ce56b8f280ddcb080dfd91df01043"
], ],
"index": "pypi", "index": "pypi",
"version": "==6.0.1" "version": "==6.0.2"
}, },
"pytest-tornasync": { "pytest-tornasync": {
"hashes": [ "hashes": [

6
src/client.py Normal file
View File

@ -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)

View File

@ -5,12 +5,12 @@ from solana.publickey import PublicKey
from solana.sysvar import SYSVAR_RENT_PUBKEY from solana.sysvar import SYSVAR_RENT_PUBKEY
from solana.transaction import AccountMeta, TransactionInstruction from solana.transaction import AccountMeta, TransactionInstruction
from solana.utils.validate import validate_instruction_keys, validate_instruction_type 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 ._layouts.instructions import INSTRUCTIONS_LAYOUT, InstructionType
from .enums import OrderType, Side from .enums import OrderType, Side
DEFAULT_DEX_PROGRAM_ID = PublicKey("4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn") DEFAULT_DEX_PROGRAM_ID = PublicKey("4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn")
TOKEN_PROGRAM_ID = PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
class InitializeMarketParams(NamedTuple): class InitializeMarketParams(NamedTuple):

View File

@ -5,6 +5,7 @@ import logging
import math import math
from typing import Any, Iterable, List, NamedTuple from typing import Any, Iterable, List, NamedTuple
from construct import Struct as cStruct # type: ignore
from solana.account import Account from solana.account import Account
from solana.publickey import PublicKey from solana.publickey import PublicKey
from solana.rpc.api import Client 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 cancel_order as cancel_order_inst
from .instructions import match_orders as match_order_inst from .instructions import match_orders as match_order_inst
from .instructions import new_order as new_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 .queue_ import decode_event_queue, decode_request_queue
from .utils import load_bytes_data from .utils import load_bytes_data
@ -40,11 +41,11 @@ class Market:
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
def __init__( def __init__(
self, self,
decoded: Any, decoded: Any, # Construct structure of the market.
base_mint_decimals: int, base_mint_decimals: int,
quote_mint_decimals: int, quote_mint_decimals: int,
options: Any, # pylint: disable=unused-argument options: Any, # pylint: disable=unused-argument
endpoint: str, conn: Client,
program_id: PublicKey = DEFAULT_DEX_PROGRAM_ID, program_id: PublicKey = DEFAULT_DEX_PROGRAM_ID,
) -> None: ) -> None:
# TODO: add options # TODO: add options
@ -56,25 +57,28 @@ class Market:
self._skip_preflight = False self._skip_preflight = False
self._confirmations = 10 self._confirmations = 10
self._program_id = program_id 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 @staticmethod
# pylint: disable=unused-argument # pylint: disable=unused-argument
def load( def load(conn: Client, market_address: str, options: Any, program_id: PublicKey = DEFAULT_DEX_PROGRAM_ID) -> Market:
endpoint: str, market_address: str, options: Any, program_id: PublicKey = DEFAULT_DEX_PROGRAM_ID
) -> Market:
"""Factory method to create a 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) market_state = MARKET_LAYOUT.parse(bytes_data)
# TODO: add ownAddress check! # TODO: add ownAddress check!
if not market_state.account_flags.initialized or not market_state.account_flags.market: if not market_state.account_flags.initialized or not market_state.account_flags.market:
raise Exception("Invalid market") raise Exception("Invalid market")
base_mint_decimals = Market.get_mint_decimals(endpoint, PublicKey(market_state.base_mint)) base_mint_decimals = Market.get_mint_decimals(conn, PublicKey(market_state.base_mint))
quote_mint_decimals = Market.get_mint_decimals(endpoint, PublicKey(market_state.quote_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: def address(self) -> PublicKey:
"""Return market address.""" """Return market address."""
@ -107,7 +111,7 @@ class Market:
return PublicKey(self._decode.request_queue) return PublicKey(self._decode.request_queue)
def event_queue(self) -> PublicKey: def event_queue(self) -> PublicKey:
"""Returns quote vault address.""" """Returns event queue address."""
return PublicKey(self._decode.event_queue) return PublicKey(self._decode.event_queue)
def __base_spl_token_multiplier(self) -> int: 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) return int(math.floor(size * 10 ** self._base_spl_token_decimals) / self._decode.base_lot_size)
@staticmethod @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.""" """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 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: def load_bids(self) -> OrderBook:
"""Load the bid order book""" """Load the bid order book"""
bids_addr = PublicKey(self._decode.bids) 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) return OrderBook.decode(self, bytes_data)
def load_asks(self) -> OrderBook: def load_asks(self) -> OrderBook:
"""Load the Ask order book.""" """Load the Ask order book."""
asks_addr = PublicKey(self._decode.asks) 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) 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 def load_event_queue(self): # returns raw construct type
event_queue_addr = PublicKey(self._decode.event_queue) 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) return decode_event_queue(bytes_data)
def load_request_queue(self): # returns raw construct type def load_request_queue(self): # returns raw construct type
request_queue_addr = PublicKey(self._decode.request_queue) 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) return decode_request_queue(bytes_data)
def load_fills(self, limit=100) -> List[FilledOrder]: def load_fills(self, limit=100) -> List[FilledOrder]:
event_queue_addr = PublicKey(self._decode.event_queue) 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) events = decode_event_queue(bytes_data, limit)
return [ return [
self.parse_fill_event(event) self.parse_fill_event(event)
@ -216,24 +238,27 @@ class Market:
limit_price: int, limit_price: int,
max_quantity: int, max_quantity: int,
client_id: int = 0, client_id: int = 0,
): ): # TODO: Add open_orders_address_key param
transaction = Transaction() transaction = Transaction()
signers: List[Account] = [owner] signers: List[Account] = [owner]
open_order_accounts = self.find_open_orders_accounts_for_owner(owner.public_key()) open_order_accounts = self.find_open_orders_accounts_for_owner(owner.public_key())
if not open_order_accounts: if not open_order_accounts:
new_open_order_account = Account() 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( transaction.add(
make_create_account_instruction( make_create_account_instruction(
owner.public_key(), owner.public_key(),
new_open_order_account.public_key(), new_open_order_account.public_key(),
Client(self._endpoint).get_minimum_balance_for_rent_exemption(OPEN_ORDERS_LAYOUT.sizeof())[ balanced_needed,
"result"
],
self._program_id, self._program_id,
) )
) )
signers.append(new_open_order_account) signers.append(new_open_order_account)
# TODO: Handle open_orders_address_key
# TODO: Handle wrapped sol account
transaction.add( transaction.add(
self.make_place_order_instruction( self.make_place_order_instruction(
payer, 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: 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: def cancel_order(self, owner: Account, order: Order) -> str:
transaction = Transaction() txn = Transaction().add(self.make_cancel_order_instruction(owner.public_key(), order))
transaction.add(self.make_cancel_order_instruction(owner.public_key(), order)) return self._send_transaction(txn, owner)
return self._send_transaction(transaction, owner)
def match_orders(self, fee_payer: Account, limit: int) -> str: def match_orders(self, fee_payer: Account, limit: int) -> str:
transaction = Transaction() txn = Transaction().add(self.make_match_orders_instruction(limit))
transaction.add(self.make_match_orders_instruction(limit)) return self._send_transaction(txn, fee_payer)
return self._send_transaction(transaction, fee_payer)
def make_cancel_order_instruction(self, owner: PublicKey, order: Order) -> TransactionInstruction: def make_cancel_order_instruction(self, owner: PublicKey, order: Order) -> TransactionInstruction:
params = CancelOrderParams( params = CancelOrderParams(
@ -326,9 +344,13 @@ class Market:
) )
return match_order_inst(params) 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: def _send_transaction(self, transaction: Transaction, *signers: Account) -> str:
connection = Client(self._endpoint) res = self._conn.send_transaction(transaction, *signers, skip_preflight=self._skip_preflight)
res = connection.send_transaction(transaction, *signers, skip_preflight=self._skip_preflight)
if self._confirmations > 0: if self._confirmations > 0:
self.logger.warning("Cannot confirm transaction yet.") self.logger.warning("Cannot confirm transaction yet.")
signature = res.get("result") signature = res.get("result")

View File

@ -21,7 +21,7 @@ class ProgramAccount(NamedTuple):
owner: PublicKey owner: PublicKey
class OpenOrderAccount: class OpenOrdersAccount:
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
# pylint: disable=too-many-instance-attributes # pylint: disable=too-many-instance-attributes
def __init__( def __init__(
@ -51,12 +51,12 @@ class OpenOrderAccount:
self.client_ids = client_ids self.client_ids = client_ids
@staticmethod @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) 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: 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.") raise Exception("Not an open order account or not initialized.")
return OpenOrderAccount( return OpenOrdersAccount(
address=address, address=address,
market=PublicKey(open_order_decoded.market), market=PublicKey(open_order_decoded.market),
owner=PublicKey(open_order_decoded.owner), owner=PublicKey(open_order_decoded.owner),
@ -72,8 +72,8 @@ class OpenOrderAccount:
@staticmethod @staticmethod
def find_for_market_and_owner( def find_for_market_and_owner(
endpoint: str, market: PublicKey, owner: PublicKey, program_id: PublicKey conn: Client, market: PublicKey, owner: PublicKey, program_id: PublicKey
) -> List[OpenOrderAccount]: ) -> List[OpenOrdersAccount]:
filters = [ filters = [
MemcmpOpt( MemcmpOpt(
offset=5 + 8, # 5 bytes of padding, 8 bytes of account flag 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() program_id, encoding="base64", memcmp_opts=filters, data_size=OPEN_ORDERS_LAYOUT.sizeof()
) )
accounts = [] accounts = []
@ -100,13 +100,13 @@ class OpenOrderAccount:
lamports=int(account_details["lamports"]), 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 @staticmethod
def load(endpoint: str, address: str) -> OpenOrderAccount: def load(conn: Client, address: str) -> OpenOrdersAccount:
addr_pub_key = PublicKey(address) addr_pub_key = PublicKey(address)
bytes_data = load_bytes_data(addr_pub_key, endpoint) bytes_data = load_bytes_data(addr_pub_key, conn)
return OpenOrderAccount.from_bytes(addr_pub_key, bytes_data) return OpenOrdersAccount.from_bytes(addr_pub_key, bytes_data)
def make_create_account_instruction( def make_create_account_instruction(

View File

@ -4,8 +4,8 @@ from solana.publickey import PublicKey
from solana.rpc.api import Client from solana.rpc.api import Client
def load_bytes_data(addr: PublicKey, endpoint: str): def load_bytes_data(addr: PublicKey, conn: Client):
res = Client(endpoint).get_account_info(addr) res = conn.get_account_info(addr)
if ("result" not in res) or ("value" not in res["result"]) or ("data" not in res["result"]["value"]): if ("result" not in res) or ("value" not in res["result"]) or ("data" not in res["result"]["value"]):
raise Exception("Cannot load byte data.") raise Exception("Cannot load byte data.")
data = res["result"]["value"]["data"][0] data = res["result"]["value"]["data"][0]

View File

@ -5,26 +5,26 @@ from solana.account import Account
from solana.publickey import PublicKey from solana.publickey import PublicKey
from solana.rpc.api import Client from solana.rpc.api import Client
__cached_params = {} from src.client import market_client
@pytest.mark.integration @pytest.mark.integration
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def __bs_params() -> Dict[str, str]: def __bs_params() -> Dict[str, str]:
if not __cached_params: params = {}
with open("tests/crank.log") as crank_log: with open("tests/crank.log") as crank_log:
for line in crank_log.readlines(): for line in crank_log.readlines():
if ":" not in line: if ":" not in line:
continue continue
key, val = line.strip().replace(",", "").split(": ") key, val = line.strip().replace(",", "").split(": ")
assert key, "key must not be None" assert key, "key must not be None"
assert val, "val must not be None" assert val, "val must not be None"
__cached_params[key] = val params[key] = val
return __cached_params return params
def __bootstrap_account(pubkey: str, secret: str) -> Account: def __bootstrap_account(pubkey: str, secretkey: str) -> Account:
secret = [int(b) for b in secret[1:-1].split(" ")] secret = [int(b) for b in secretkey[1:-1].split(" ")]
account = Account(secret) account = Account(secret)
assert str(account.public_key()) == pubkey, "account must map to provided public key" assert str(account.public_key()) == pubkey, "account must map to provided public key"
return account return account
@ -146,5 +146,7 @@ def stubbed_ask_account_pk(__bs_params) -> PublicKey:
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def http_client() -> Client: def http_client() -> Client:
"""Solana http 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 return client

View File

@ -11,9 +11,9 @@ from .utils import confirm_transaction
@pytest.mark.integration @pytest.mark.integration
@pytest.fixture(scope="session") @pytest.fixture(scope="module")
def bootstrapped_market(stubbed_market_pk: PublicKey, stubbed_dex_program_pk: PublicKey) -> Market: def bootstrapped_market(http_client: Client, 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) return Market.load(http_client, str(stubbed_market_pk), None, stubbed_dex_program_pk)
@pytest.mark.integration @pytest.mark.integration

View File

@ -1,36 +1,54 @@
import base64 import base64
from types import SimpleNamespace from types import SimpleNamespace
import pytest
from solana.rpc.api import Client
from src.market import MARKET_LAYOUT, Market, Order, OrderBook from src.market import MARKET_LAYOUT, Market, Order, OrderBook
from .binary_file_path import ASK_ORDER_BIN_PATH 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( @pytest.fixture(scope="module")
**{ def stubbed_data() -> bytes:
"account_flags": SimpleNamespace( MARKET_DATA_HEX = ( # pylint: disable=invalid-name
**{ "736572756d030000000000000054f23cbc4d93795ce75ecd8173bcf436923112f7b6b024f3afd9b6789124b9680000000000"
"initialized": True, "000000c73690f1d4b87aa9337848369c20a682b37e8dcb33f4237a2a8f8b0abd64bd1cd9c75c9a58645ff02ab0741cd6d179"
"market": True, "0067957d2266165b767259b358ded270fb2dbc6d44f2ab58dce432b5bb31d98517366d6e24e69c0bf5b926ead9ec65893540"
"bids": False, "8ffd71c50000000000000000000000e1a461a046199877c4cd3cbafc61c3dfdb088e737b7193a3d28e72b709421fc4544fec"
} "927c000000a4fc4c12000000006400000000000000f422ea23ada9e1a9d100ba8443deb041c231e0f79ee6d07d1c1f7042fe"
), "4a1ade3b236ea4ba636227dfa22773f41fa02cc91842c2e9330e2ac0a987dc68b520e8c58676b5751c48e22c3bcc6edda8f7"
"vault_signer_nonce": 0, "5f76b1596b9874bd5714366e32d84e3bc0400501895361982c4be67d03af519ac7fd96a8a79f5b15ec7af79f6b70290bf740"
"base_fees_accrued": 0, "420f00000000001027000000000000000000000000000070616464696e67"
"quote_dust_threshold": 100, )
"base_lot_size": 100, return bytes.fromhex(MARKET_DATA_HEX)
"quote_lot_size": 10,
"fee_rate_bps": 0,
}
)
BTC_USDC_MARKET = Market(MARKET_ENCODE, 6, 6, None, "http://stubbed_endpoint:123/")
def test_parse_market_state(): @pytest.fixture(scope="module")
parsed_market = MARKET_LAYOUT.parse(DATA) 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.initialized
assert parsed_market.account_flags.market assert parsed_market.account_flags.market
assert not parsed_market.account_flags.open_orders assert not parsed_market.account_flags.open_orders
@ -40,31 +58,31 @@ def test_parse_market_state():
assert parsed_market.fee_rate_bps == 0 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.""" """Test order book parsing."""
with open(ASK_ORDER_BIN_PATH, "r") as input_file: with open(ASK_ORDER_BIN_PATH, "r") as input_file:
base64_res = input_file.read() base64_res = input_file.read()
data = base64.decodebytes(base64_res.encode("ascii")) 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()]) total_orders = sum([1 for _ in order_book.orders()])
assert total_orders == 15 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: with open(ASK_ORDER_BIN_PATH, "r") as input_file:
base64_res = input_file.read() base64_res = input_file.read()
data = base64.decodebytes(base64_res.encode("ascii")) 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): for i in range(1, 16):
assert i == len(order_book.get_l2(i)) assert i == len(order_book.get_l2(i))
assert [(11744.6, 4.0632, 117446, 40632)] == order_book.get_l2(1) 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: with open(ASK_ORDER_BIN_PATH, "r") as input_file:
base64_res = input_file.read() base64_res = input_file.read()
data = base64.decodebytes(base64_res.encode("ascii")) data = base64.decodebytes(base64_res.encode("ascii"))
order_book = OrderBook.decode(BTC_USDC_MARKET, data) order_book = OrderBook.decode(stubbed_market, data)
cnt = 0 cnt = 0
for order in order_book: for order in order_book:
cnt += 1 cnt += 1

View File

@ -2,7 +2,7 @@ import base64
from solana.publickey import PublicKey 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 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: with open(OPEN_ORDER_ACCOUNT_BIN_PATH, "r") as input_file:
base64_res = input_file.read() base64_res = input_file.read()
data = base64.decodebytes(base64_res.encode("ascii")) 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.market == PublicKey("4r5Bw3HxmxAzPQ2ATUvgF2nFe3B6G1Z2Nq2Nwu77wWc2")
assert open_order_account.owner == PublicKey("7hJx7QMiVfjZSSADQ18oNKzqifJPMu18djYLkh4aYh5Q") assert open_order_account.owner == PublicKey("7hJx7QMiVfjZSSADQ18oNKzqifJPMu18djYLkh4aYh5Q")
assert len([order for order in open_order_account.orders if order != 0]) == 3 assert len([order for order in open_order_account.orders if order != 0]) == 3