Added order placement API. (#31)

Added order placement API including finding the open order account logic.
This commit is contained in:
Leonard G 2020-09-16 23:04:28 +08:00 committed by GitHub
parent 969c14f3e1
commit e2351f5134
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 262 additions and 15 deletions

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import logging
import math
from typing import Any, Iterable, List, NamedTuple, Tuple
from typing import Any, Iterable, List, NamedTuple
from solana.account import Account
from solana.publickey import PublicKey
@ -12,11 +12,14 @@ from solana.transaction import Transaction, TransactionInstruction
from ._layouts.account_flags import ACCOUNT_FLAGS_LAYOUT
from ._layouts.market import MARKET_LAYOUT, MINT_LAYOUT
from ._layouts.open_orders import OPEN_ORDERS_LAYOUT
from ._layouts.slab import Slab
from .enums import Side
from .enums import OrderType, Side
from .instructions import DEFAULT_DEX_PROGRAM_ID, CancelOrderParams, MatchOrdersParams, NewOrderParams
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 .queue_ import decode_event_queue, decode_request_queue
from .utils import load_bytes_data
@ -91,6 +94,22 @@ class Market:
"""Returns quote mint address."""
return PublicKey(self._decode.quote_mint)
def base_vault_address(self) -> PublicKey:
"""Returns base vault address."""
return PublicKey(self._decode.base_vault)
def quote_vault_address(self) -> PublicKey:
"""Returns quote vault address."""
return PublicKey(self._decode.quote_vault)
def request_queue(self) -> PublicKey:
"""Returns quote vault address."""
return PublicKey(self._decode.request_queue)
def event_queue(self) -> PublicKey:
"""Returns quote vault address."""
return PublicKey(self._decode.event_queue)
def __base_spl_token_multiplier(self) -> int:
return 10 ** self._base_spl_token_decimals
@ -109,7 +128,12 @@ class Market:
)
def price_number_to_lots(self, price: float) -> int:
raise NotImplementedError("price_number_to_lots is not implemented")
return int(
round(
(price * 10 ** self.__quote_spl_token_multiplier() * self._decode.base_lot_size)
/ (10 ** self.__base_spl_token_multiplier() * self._decode.quote_lot_size)
)
)
def base_size_lots_to_number(self, size: int) -> float:
return float(size * self._decode.base_lot_size) / self.__base_spl_token_multiplier()
@ -183,14 +207,84 @@ class Market:
fee_cost=event.native_fee_or_rebate * (1 if event.event_flags.maker else -1),
)
def place_order(self, order_params: NewOrderParams):
pass
def place_order(
self,
payer: PublicKey,
owner: Account,
order_type: OrderType,
side: Side,
limit_price: int,
max_quantity: int,
client_id: int = 0,
):
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()
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"
],
self._program_id,
)
)
signers.append(new_open_order_account)
def make_place_order_transaction(self, order_params: NewOrderParams) -> Tuple[Transaction, List[PublicKey]]:
pass
transaction.add(
self.make_place_order_instruction(
payer,
owner,
order_type,
side,
limit_price,
max_quantity,
client_id,
open_order_accounts[0].address if open_order_accounts else new_open_order_account.public_key(),
)
)
return self._send_transaction(transaction, *signers)
def find_open_orders_accounts_for_owner(self, owner_address: PublicKey):
pass
def make_place_order_instruction(
self,
payer: PublicKey,
owner: Account,
order_type: OrderType,
side: Side,
limit_price: int,
max_quantity: int,
client_id: int,
open_order_account: PublicKey,
) -> TransactionInstruction:
if self.base_size_number_to_lots(max_quantity) < 0:
raise Exception("Size lot %d is too small." % max_quantity)
if self.price_number_to_lots(limit_price) < 0:
raise Exception("Price lot %d is too small." % limit_price)
return new_order_inst(
NewOrderParams(
market=self.address(),
open_orders=open_order_account,
payer=payer,
owner=owner.public_key(),
request_queue=self.request_queue(),
base_vault=self.base_vault_address(),
quote_vault=self.quote_vault_address(),
side=side,
limit_price=limit_price,
max_quantity=max_quantity,
order_type=order_type,
client_id=client_id,
program_id=self._program_id,
)
)
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

View File

@ -1,14 +1,26 @@
from __future__ import annotations
from typing import List
import base64
from typing import List, NamedTuple
from solana.publickey import PublicKey
from solana.rpc.api import Client
from solana.rpc.api import Client, MemcmpOpt
from solana.system_program import CreateAccountParams, create_account
from solana.transaction import TransactionInstruction
from ._layouts.open_orders import OPEN_ORDERS_LAYOUT
from .instructions import DEFAULT_DEX_PROGRAM_ID
from .utils import load_bytes_data
class ProgramAccount(NamedTuple):
public_key: PublicKey
data: bytes
is_executablable: bool
lamports: int
owner: PublicKey
class OpenOrderAccount:
# pylint: disable=too-many-arguments
# pylint: disable=too-many-instance-attributes
@ -41,6 +53,9 @@ class OpenOrderAccount:
@staticmethod
def from_bytes(address: PublicKey, data_bytes: bytes) -> OpenOrderAccount:
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(
address=address,
market=PublicKey(open_order_decoded.market),
@ -56,11 +71,56 @@ class OpenOrderAccount:
)
@staticmethod
def find_for_market_and_owner(connection: Client, market: PublicKey, owner: PublicKey):
pass
def find_for_market_and_owner(
endpoint: str, market: PublicKey, owner: PublicKey, program_id: PublicKey
) -> List[OpenOrderAccount]:
filters = [
MemcmpOpt(
offset=5 + 8, # 5 bytes of padding, 8 bytes of account flag
bytes=str(market),
),
MemcmpOpt(
offset=5 + 8 + 32, # 5 bytes of padding, 8 bytes of account flag, 32 bytes of market public key
bytes=str(owner),
),
]
resp = Client(endpoint).get_program_accounts(
program_id, encoding="base64", memcmp_opts=filters, data_size=OPEN_ORDERS_LAYOUT.sizeof()
)
accounts = []
for account in resp["result"]:
account_details = account["account"]
accounts.append(
ProgramAccount(
public_key=PublicKey(account["pubkey"]),
data=base64.decodebytes(account_details["data"][0].encode("ascii")),
is_executablable=bool(account_details["executable"]),
owner=PublicKey(account_details["owner"]),
lamports=int(account_details["lamports"]),
)
)
return [OpenOrderAccount.from_bytes(account.public_key, account.data) for account in accounts]
@staticmethod
def load(endpoint: str, address: str) -> OpenOrderAccount:
addr_pub_key = PublicKey(address)
bytes_data = load_bytes_data(addr_pub_key, endpoint)
return OpenOrderAccount.from_bytes(addr_pub_key, bytes_data)
def make_create_account_instruction(
owner_address: PublicKey,
new_account_address: PublicKey,
lamports: int,
program_id: PublicKey = DEFAULT_DEX_PROGRAM_ID,
) -> TransactionInstruction:
return create_account(
CreateAccountParams(
from_pubkey=owner_address,
new_account_pubkey=new_account_address,
lamports=lamports,
space=OPEN_ORDERS_LAYOUT.sizeof(),
program_id=program_id,
)
)

View File

@ -13,12 +13,12 @@ def _decode_queue(header_layout: Any, node_layout: Any, buffer: bytes, history:
for i in range(min(history, alloc_len)):
node_index = (header.head + header.count + alloc_len - 1 - i) % alloc_len
offset = header_layout.sizeof() + node_index * node_layout.sizeof()
nodes.append(node_layout.parse(buffer[offset : offset + node_layout.sizeof()])) # noqa: E203 # noqa: E203
nodes.append(node_layout.parse(buffer[offset : offset + node_layout.sizeof()])) # noqa: E203
else:
for i in range(header.count):
node_index = (header.head + i) % alloc_len
offset = header_layout.sizeof() + node_index * node_layout.sizeof()
nodes.append(node_layout.parse(buffer[offset : offset + node_layout.sizeof()])) # noqa: E203 # noqa: E203
nodes.append(node_layout.parse(buffer[offset : offset + node_layout.sizeof()])) # noqa: E203
return header, nodes

View File

@ -0,0 +1 @@
c2VydW0FAAAAAAAAADklQaAz6ADd3zS9MiJs/L7mmunSOSZtNjcUVqTd25yTY3mmdwHMScQ6cpqR7agJJnqKc3TW62zaypJkFqLsl+cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAs27bCDgAAAPj///////////////////8HAAAAAAAAAAAAAAAAAAAA///////////0AQAAAAAAAP3/////////COIBAAAAAAD8/////////9IEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFSwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcGFkZGluZw==

View File

@ -1,3 +1,4 @@
_BINARY_DIR_PATH = "tests/binary/"
ASK_ORDER_BIN_PATH = _BINARY_DIR_PATH + "ask_order_binary.bin"
EVENT_QUEUE_BIN_PATH = _BINARY_DIR_PATH + "event_queue_binary.bin"
OPEN_ORDER_ACCOUNT_BIN_PATH = _BINARY_DIR_PATH + "open_order_account_binary.bin"

View File

@ -4,6 +4,7 @@ from solana.account import Account
from solana.publickey import PublicKey
from solana.rpc.api import Client
from src.enums import OrderType, Side
from src.market import Market
from .utils import confirm_transaction
@ -77,3 +78,57 @@ def test_match_order(bootstrapped_market: Market, stubbed_payer: Account, http_c
# There should be no ask order.
asks = bootstrapped_market.load_asks()
assert sum(1 for _ in asks) == 0
@pytest.mark.integration
def test_new_order(
bootstrapped_market: Market,
stubbed_payer: Account,
http_client: Client,
stubbed_quote_wallet: Account,
stubbed_base_wallet: Account,
):
initial_request_len = len(bootstrapped_market.load_request_queue())
sig = bootstrapped_market.place_order(
payer=stubbed_quote_wallet.public_key(),
owner=stubbed_payer,
side=Side.Buy,
order_type=OrderType.Limit,
limit_price=1000,
max_quantity=3000,
)
confirm_transaction(http_client, sig)
request_queue = bootstrapped_market.load_request_queue()
# 0 request after matching.
assert len(request_queue) == initial_request_len + 1
# There should be no bid order.
bids = bootstrapped_market.load_bids()
assert sum(1 for _ in bids) == 0
# There should be no ask order.
asks = bootstrapped_market.load_asks()
assert sum(1 for _ in asks) == 0
sig = bootstrapped_market.place_order(
payer=stubbed_base_wallet.public_key(),
owner=stubbed_payer,
side=Side.Sell,
order_type=OrderType.Limit,
limit_price=1500,
max_quantity=3000,
)
confirm_transaction(http_client, sig)
# The two order shouldn't get executed since there is a price difference of 1
sig = bootstrapped_market.match_orders(stubbed_payer, 2)
confirm_transaction(http_client, sig)
# There should be 1 bid order that we sent earlier.
bids = bootstrapped_market.load_bids()
assert sum(1 for _ in bids) == 1
# There should be 1 ask order that we sent earlier.
asks = bootstrapped_market.load_asks()
assert sum(1 for _ in asks) == 1

View File

@ -0,0 +1,36 @@
import base64
from solana.publickey import PublicKey
from src.open_order_account import OPEN_ORDERS_LAYOUT, OpenOrderAccount
from .binary_file_path import OPEN_ORDER_ACCOUNT_BIN_PATH
def test_decode_open_order_account_layout():
"""Test decode event queue."""
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 = OPEN_ORDERS_LAYOUT.parse(data)
assert open_order_account.account_flags.open_orders
assert open_order_account.account_flags.initialized
assert PublicKey(open_order_account.market) == PublicKey("4r5Bw3HxmxAzPQ2ATUvgF2nFe3B6G1Z2Nq2Nwu77wWc2")
assert PublicKey(open_order_account.owner) == PublicKey("7hJx7QMiVfjZSSADQ18oNKzqifJPMu18djYLkh4aYh5Q")
# if there is no order the byte returned here will be all 0. In this case we have three orders.
assert len([order for order in open_order_account.orders if int.from_bytes(order, "little") != 0]) == 3
# the first three order are bid order
assert int.from_bytes(open_order_account.is_bid_bits, "little") == 0b111
def test_decode_open_order_account():
"""Test decode event queue."""
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)
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
# the first three order are bid order
assert open_order_account.is_bid_bits == 0b111