Added SOL wrapping and cancel order by client id. (#40)
Added SOL wrapping and cancel_order_by_client_id.
This commit is contained in:
parent
444f89d899
commit
34d7401d2c
|
@ -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"
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
""""""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue