Added SOL wrapping and cancel order by client id. (#40)

Added SOL wrapping and cancel_order_by_client_id.
This commit is contained in:
Leonard G 2020-09-24 23:11:00 +08:00 committed by GitHub
parent 444f89d899
commit 34d7401d2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 178 additions and 21 deletions

View File

@ -22,12 +22,12 @@ if ! hash solana 2>/dev/null; then
solana-keygen new -o ~/.config/solana/id.json --no-passphrase --silent
fi
solana config set --url "http://localhost:8899"
curl -s -L "https://github.com/serum-community/serum-dex/releases/download/refs%2Fheads%2Fmaster/serum_dex-$os_type.so" > serum_dex.so
curl -s -L "https://github.com/serum-community/serum-dex/releases/download/v1/serum_dex-$os_type.so" > serum_dex.so
sleep 1
solana airdrop 10000
DEX_PROGRAM_ID="$(solana deploy --use-deprecated-loader serum_dex.so | jq .programId -r)"
echo DEX_PROGRAM_ID: "$DEX_PROGRAM_ID"
curl -s -L "https://github.com/serum-community/serum-dex/releases/download/refs%2Fheads%2Fmaster/crank-$os_type" > crank
curl -s -L "https://github.com/serum-community/serum-dex/releases/download/v1/crank-$os_type" > crank
chmod +x crank
./crank l pyserum-setup ~/.config/solana/id.json "$DEX_PROGRAM_ID"

View File

@ -20,7 +20,7 @@ REQUEST_FLAGS_LAYOUT = BitsSwapped( # Swap to little endian
"new_order" / Flag,
"cancel_order" / Flag,
"bid" / Flag,
"postOnly" / Flag,
"post_only" / Flag,
"ioc" / Flag,
Const(0, BitsInteger(3)), # Padding
)

View File

@ -266,7 +266,7 @@ def decode_cancel_order(instruction: TransactionInstruction) -> CancelOrderParam
request_queue=instruction.keys[2].pubkey,
owner=instruction.keys[3].pubkey,
side=Side(data.args.side),
order_id=int.from_bytes(data.args.order_id, "big"),
order_id=int.from_bytes(data.args.order_id, "little"),
open_orders_slot=data.args.open_orders_slot,
)
@ -405,7 +405,7 @@ def cancel_order(params: CancelOrderParams) -> TransactionInstruction:
instruction_type=InstructionType.CancelOrder,
args=dict(
side=params.side,
order_id=params.order_id,
order_id=params.order_id.to_bytes(16, byteorder="little"),
open_orders=bytes(params.open_orders),
open_orders_slot=params.open_orders_slot,
),

View File

@ -3,9 +3,10 @@ from enum import IntEnum
from typing import List, Optional, Sequence, Tuple, Union, cast
from construct import Container # type: ignore
from solana.publickey import PublicKey
from ..._layouts.queue import EVENT_LAYOUT, QUEUE_HEADER_LAYOUT, REQUEST_LAYOUT
from ..types import Event, Request
from ..types import Event, EventFlags, Request, ReuqestFlags
class QueueType(IntEnum):
@ -18,7 +19,7 @@ def __from_bytes(
) -> Tuple[Container, List[Union[Event, Request]]]:
header = QUEUE_HEADER_LAYOUT.parse(buffer)
layout_size = EVENT_LAYOUT.sizeof() if queue_type == QueueType.Event else REQUEST_LAYOUT.sizeof()
alloc_len = math.floor(len(buffer) - QUEUE_HEADER_LAYOUT.sizeof() / layout_size)
alloc_len = math.floor((len(buffer) - QUEUE_HEADER_LAYOUT.sizeof()) / layout_size)
nodes: List[Union[Event, Request]] = []
if history:
for i in range(min(history, alloc_len)):
@ -36,7 +37,47 @@ def __from_bytes(
def __parse_queue_item(buffer: Sequence[int], queue_type: QueueType) -> Union[Event, Request]:
layout = EVENT_LAYOUT if queue_type == QueueType.Event else REQUEST_LAYOUT
parsed_item = layout.parse(buffer)
parsed_item.pop("_io") # Hack: Drop BytesIO object to fit kwargs into Event/Request object.
if queue_type == QueueType.Event: # pylint: disable=no-else-return
parsed_event_flags = parsed_item.event_flags
event_flags = EventFlags(
fill=parsed_event_flags.fill,
out=parsed_event_flags.out,
bid=parsed_event_flags.bid,
maker=parsed_event_flags.maker,
)
return Event(
event_flags=event_flags,
open_order_slot=parsed_item.open_order_slot,
fee_tier=parsed_item.fee_tier,
native_quantity_released=parsed_item.native_quantity_released,
native_quantity_paid=parsed_item.native_quantity_paid,
native_fee_or_rebate=parsed_item.native_fee_or_rebate,
order_id=int.from_bytes(parsed_item.order_id, "little"),
public_key=PublicKey(parsed_item.public_key),
client_order_id=parsed_item.client_order_id,
)
else:
parsed_request_flags = parsed_item.request_flags
request_flags = ReuqestFlags(
new_order=parsed_request_flags.new_order,
cancel_order=parsed_request_flags.cancel_order,
bid=parsed_request_flags.bid,
post_only=parsed_request_flags.post_only,
ioc=parsed_request_flags.ioc,
)
return Request(
request_flags=request_flags,
open_order_slot=parsed_item.open_order_slot,
fee_tier=parsed_item.fee_tier,
max_base_size_or_cancel_id=parsed_item.max_base_size_or_cancel_id,
native_quote_quantity_locked=parsed_item.native_quote_quantity_locked,
order_id=int.from_bytes(parsed_item.order_id, "little"),
open_orders=PublicKey(parsed_item.open_orders),
client_order_id=parsed_item.client_order_id,
)
return Event(**parsed_item) if queue_type == QueueType.Event else Request(**parsed_item)

View File

@ -7,8 +7,12 @@ from typing import List
from solana.account import Account
from solana.publickey import PublicKey
from solana.rpc.api import Client
from solana.system_program import CreateAccountParams, create_account
from solana.sysvar import SYSVAR_RENT_PUBKEY
from solana.transaction import Transaction, TransactionInstruction
from spl.token.constants import WRAPPED_SOL_MINT # type: ignore # TODO: Remove ignore.
from spl.token.constants import ACCOUNT_LEN, TOKEN_PROGRAM_ID, WRAPPED_SOL_MINT # type: ignore # TODO: Remove ignore.
from spl.token.instructions import CloseAccountParams # type: ignore
from spl.token.instructions import InitializeAccountParams, close_account, initialize_account
import src.instructions as instructions
import src.market.types as t
@ -21,6 +25,8 @@ from ._internal.queue import decode_event_queue, decode_request_queue
from .orderbook import OrderBook
from .state import MarketState
LAMPORTS_PER_SOL = 1000000000
# pylint: disable=too-many-public-methods
class Market:
@ -130,7 +136,7 @@ class Market:
fee_cost=event.native_fee_or_rebate * (1 if event.event_flags.maker else -1),
)
def place_order( # pylint: disable=too-many-arguments
def place_order( # pylint: disable=too-many-arguments,too-many-locals
self,
payer: PublicKey,
owner: Account,
@ -163,15 +169,40 @@ class Market:
if payer == owner.public_key():
raise ValueError("Invalid payer account")
if (side == side.Buy and self.state.quote_mint() == WRAPPED_SOL_MINT) or (
# TODO: add integration test for SOL wrapping.
should_wrap_sol = (side == side.Buy and self.state.quote_mint() == WRAPPED_SOL_MINT) or (
side == side.Sell and self.state.base_mint == WRAPPED_SOL_MINT
):
# TODO: Handle wrapped sol account
raise NotImplementedError("WRAPPED_SOL_MINT is currently unsupported")
)
wrapped_sol_account = Account()
if should_wrap_sol:
transaction.add(
create_account(
CreateAccountParams(
from_pubkey=owner.public_key(),
new_account_pubkey=wrapped_sol_account.public_key(),
lamports=Market._get_lamport_need_for_sol_wrapping(
limit_price, max_quantity, side, open_order_accounts
),
space=ACCOUNT_LEN,
program_id=TOKEN_PROGRAM_ID,
)
)
)
transaction.add(
initialize_account(
InitializeAccountParams(
account=wrapped_sol_account.public_key(),
mint=WRAPPED_SOL_MINT,
owner=owner.public_key(),
program_id=SYSVAR_RENT_PUBKEY,
)
)
)
transaction.add(
self.make_place_order_instruction(
payer,
wrapped_sol_account.public_key() if should_wrap_sol else payer,
owner,
order_type,
side,
@ -181,8 +212,36 @@ class Market:
open_order_accounts[0].address if open_order_accounts else new_open_orders_account.public_key(),
)
)
if should_wrap_sol:
transaction.add(
close_account(
CloseAccountParams(
account=wrapped_sol_account.public_key(),
owner=owner.public_key(),
dest=owner.public_key(),
)
)
)
# TODO: extract `make_place_order_transaction`.
return self._send_transaction(transaction, *signers)
@staticmethod
def _get_lamport_need_for_sol_wrapping(
price: int, size: int, side: Side, open_orders_accounts: List[OpenOrdersAccount]
) -> int:
lamports = 0
if side == Side.Buy:
lamports = round(price * size * 1.01 * LAMPORTS_PER_SOL)
if open_orders_accounts:
lamports -= open_orders_accounts[0].quote_token_free
else:
lamports = round(size * LAMPORTS_PER_SOL)
if open_orders_accounts:
lamports -= open_orders_accounts[0].base_token_free
return max(lamports, 0) + 10000000
def make_place_order_instruction( # pylint: disable=too-many-arguments
self,
payer: PublicKey,
@ -216,8 +275,24 @@ class Market:
)
)
def cancel_order_by_client_id(self, owner: str) -> str:
raise NotImplementedError("cancel_order_by_client_id not implemented")
def cancel_order_by_client_id(self, owner: Account, open_orders_account: PublicKey, client_id: int) -> str:
txs = Transaction()
txs.add(self.make_cancel_order_by_client_id_instruction(owner, open_orders_account, client_id))
return self._send_transaction(txs, owner)
def make_cancel_order_by_client_id_instruction(
self, owner: Account, open_orders_account: PublicKey, client_id: int
) -> TransactionInstruction:
return instructions.cancel_order_by_client_id(
instructions.CancelOrderByClientIDParams(
market=self.state.public_key(),
owner=owner.public_key(),
open_orders=open_orders_account,
request_queue=self.state.request_queue(),
client_id=client_id,
program_id=self.state.program_id(),
)
)
def cancel_order(self, owner: Account, order: t.Order) -> str:
txn = Transaction().add(self.make_cancel_order_instruction(owner.public_key(), order))

View File

@ -2,7 +2,6 @@ from __future__ import annotations
from typing import NamedTuple, Sequence
from construct import Container # type: ignore
from solana.publickey import PublicKey
from .._layouts.account_flags import ACCOUNT_FLAGS_LAYOUT
@ -87,8 +86,16 @@ class Order(NamedTuple):
""""""
class ReuqestFlags(NamedTuple):
new_order: bool
cancel_order: bool
bid: bool
post_only: bool
ioc: bool
class Request(NamedTuple):
request_flags: Container # TODO: Remove container type
request_flags: ReuqestFlags
""""""
open_order_slot: int
""""""
@ -106,8 +113,15 @@ class Request(NamedTuple):
""""""
class EventFlags(NamedTuple):
fill: bool
out: bool
bid: bool
maker: bool
class Event(NamedTuple):
event_flags: Container # TODO: Remove container type
event_flags: EventFlags
""""""
open_order_slot: int
""""""

View File

@ -2,6 +2,7 @@ import base64
from solana.publickey import PublicKey
from solana.rpc.api import Client
from spl.token.constants import WRAPPED_SOL_MINT # type: ignore # TODO: Remove ignore.
from src._layouts.market import MINT_LAYOUT
@ -16,5 +17,8 @@ def load_bytes_data(addr: PublicKey, conn: Client):
def get_mint_decimals(conn: Client, mint_pub_key: PublicKey) -> int:
"""Get the mint decimals for a token mint"""
if mint_pub_key == WRAPPED_SOL_MINT:
return 9
bytes_data = load_bytes_data(mint_pub_key, conn)
return MINT_LAYOUT.parse(bytes_data).decimals

View File

@ -1,4 +1,5 @@
# pylint: disable=redefined-outer-name
import pytest
from solana.account import Account
from solana.publickey import PublicKey
@ -81,7 +82,7 @@ def test_match_order(bootstrapped_market: Market, stubbed_payer: Account, http_c
@pytest.mark.integration
def test_new_order(
def test_order_placement_cancellation_cycle(
bootstrapped_market: Market,
stubbed_payer: Account,
http_client: Client,
@ -132,3 +133,25 @@ def test_new_order(
# There should be 1 ask order that we sent earlier.
asks = bootstrapped_market.load_asks()
assert sum(1 for _ in asks) == 1
for bid in bids:
sig = bootstrapped_market.cancel_order(stubbed_payer, bid)
confirm_transaction(http_client, sig)
sig = bootstrapped_market.match_orders(stubbed_payer, 1)
confirm_transaction(http_client, sig)
# All bid order should have been cancelled.
bids = bootstrapped_market.load_bids()
assert sum(1 for _ in bids) == 0
for ask in asks:
sig = bootstrapped_market.cancel_order(stubbed_payer, ask)
confirm_transaction(http_client, sig)
sig = bootstrapped_market.match_orders(stubbed_payer, 1)
confirm_transaction(http_client, sig)
# All ask order should have been cancelled.
asks = bootstrapped_market.load_asks()
assert sum(1 for _ in asks) == 0