246 lines
8.4 KiB
Python
246 lines
8.4 KiB
Python
# # ⚠ Warning
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
|
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
#
|
|
# [🥭 Mango Markets](https://mango.markets/) support is available at:
|
|
# [Docs](https://docs.mango.markets/)
|
|
# [Discord](https://discord.gg/67jySBhxrg)
|
|
# [Twitter](https://twitter.com/mangomarkets)
|
|
# [Github](https://github.com/blockworks-foundation)
|
|
# [Email](mailto:hello@blockworks.foundation)
|
|
|
|
|
|
import typing
|
|
|
|
from decimal import Decimal
|
|
from pyserum.open_orders_account import OpenOrdersAccount as PySerumOpenOrdersAccount
|
|
from solana.publickey import PublicKey
|
|
from solana.rpc.types import MemcmpOpts
|
|
|
|
from .accountflags import AccountFlags
|
|
from .accountinfo import AccountInfo
|
|
from .addressableaccount import AddressableAccount
|
|
from .context import Context
|
|
from .encoding import encode_key
|
|
from .group import Group
|
|
from .layouts import layouts
|
|
from .observables import Disposable
|
|
from .placedorder import PlacedOrder
|
|
from .tokens import Token
|
|
from .version import Version
|
|
from .websocketsubscription import (
|
|
WebSocketAccountSubscription,
|
|
WebSocketSubscriptionManager,
|
|
)
|
|
|
|
|
|
# # 🥭 OpenOrders class
|
|
#
|
|
class OpenOrders(AddressableAccount):
|
|
def __init__(
|
|
self,
|
|
account_info: AccountInfo,
|
|
version: Version,
|
|
program_address: PublicKey,
|
|
account_flags: AccountFlags,
|
|
market: PublicKey,
|
|
owner: PublicKey,
|
|
base: Token,
|
|
quote: Token,
|
|
base_token_free: Decimal,
|
|
base_token_total: Decimal,
|
|
quote_token_free: Decimal,
|
|
quote_token_total: Decimal,
|
|
placed_orders: typing.Sequence[PlacedOrder],
|
|
referrer_rebate_accrued: Decimal,
|
|
) -> None:
|
|
super().__init__(account_info)
|
|
self.version: Version = version
|
|
self.program_address: PublicKey = program_address
|
|
self.account_flags: AccountFlags = account_flags
|
|
self.market: PublicKey = market
|
|
self.owner: PublicKey = owner
|
|
self.base: Token = base
|
|
self.quote: Token = quote
|
|
self.base_token_free: Decimal = base_token_free
|
|
self.base_token_total: Decimal = base_token_total
|
|
self.quote_token_free: Decimal = quote_token_free
|
|
self.quote_token_total: Decimal = quote_token_total
|
|
self.placed_orders: typing.Sequence[PlacedOrder] = placed_orders
|
|
self.referrer_rebate_accrued: Decimal = referrer_rebate_accrued
|
|
|
|
@property
|
|
def base_token_locked(self) -> Decimal:
|
|
return self.base_token_total - self.base_token_free
|
|
|
|
@property
|
|
def quote_token_locked(self) -> Decimal:
|
|
return self.quote_token_total - self.quote_token_free
|
|
|
|
# Sometimes pyserum wants to take its own PySerumOpenOrdersAccount as a parameter (e.g. in settle_funds())
|
|
def to_pyserum(self) -> PySerumOpenOrdersAccount:
|
|
return PySerumOpenOrdersAccount.from_bytes(self.address, self.account_info.data)
|
|
|
|
@staticmethod
|
|
def from_layout(
|
|
layout: typing.Any,
|
|
account_info: AccountInfo,
|
|
base: Token,
|
|
quote: Token,
|
|
) -> "OpenOrders":
|
|
account_flags = AccountFlags.from_layout(layout.account_flags)
|
|
program_address = account_info.owner
|
|
|
|
base_divisor = 10**base.decimals
|
|
quote_divisor = 10**quote.decimals
|
|
base_token_free: Decimal = layout.base_token_free / base_divisor
|
|
base_token_total: Decimal = layout.base_token_total / base_divisor
|
|
quote_token_free: Decimal = layout.quote_token_free / quote_divisor
|
|
quote_token_total: Decimal = layout.quote_token_total / quote_divisor
|
|
referrer_rebate_accrued: Decimal = (
|
|
layout.referrer_rebate_accrued / quote_divisor
|
|
)
|
|
|
|
placed_orders: typing.Sequence[PlacedOrder] = []
|
|
if account_flags.initialized:
|
|
placed_orders = PlacedOrder.build_from_open_orders_data(
|
|
layout.free_slot_bits,
|
|
layout.is_bid_bits,
|
|
layout.orders,
|
|
layout.client_ids,
|
|
)
|
|
return OpenOrders(
|
|
account_info,
|
|
Version.UNSPECIFIED,
|
|
program_address,
|
|
account_flags,
|
|
layout.market,
|
|
layout.owner,
|
|
base,
|
|
quote,
|
|
base_token_free,
|
|
base_token_total,
|
|
quote_token_free,
|
|
quote_token_total,
|
|
placed_orders,
|
|
referrer_rebate_accrued,
|
|
)
|
|
|
|
@staticmethod
|
|
def parse(account_info: AccountInfo, base: Token, quote: Token) -> "OpenOrders":
|
|
data = account_info.data
|
|
if len(data) != layouts.OPEN_ORDERS.sizeof():
|
|
raise Exception(
|
|
f"Data length ({len(data)}) does not match expected size ({layouts.OPEN_ORDERS.sizeof()})"
|
|
)
|
|
|
|
layout = layouts.OPEN_ORDERS.parse(data)
|
|
return OpenOrders.from_layout(layout, account_info, base, quote)
|
|
|
|
@staticmethod
|
|
def load_raw_open_orders_account_infos(
|
|
context: Context, group: Group
|
|
) -> typing.Dict[str, AccountInfo]:
|
|
filters = [
|
|
MemcmpOpts(
|
|
offset=layouts.ACCOUNT_FLAGS.sizeof() + 37,
|
|
bytes=encode_key(group.signer_key),
|
|
)
|
|
]
|
|
|
|
account_infos = AccountInfo.load_by_program(
|
|
context,
|
|
group.serum_program_address,
|
|
data_size=layouts.OPEN_ORDERS.sizeof(),
|
|
memcmp_opts=filters,
|
|
)
|
|
account_infos_by_address = {
|
|
key: value
|
|
for key, value in [
|
|
(str(account_info.address), account_info)
|
|
for account_info in account_infos
|
|
]
|
|
}
|
|
return account_infos_by_address
|
|
|
|
@staticmethod
|
|
def load(
|
|
context: Context,
|
|
address: PublicKey,
|
|
base: Token,
|
|
quote: Token,
|
|
) -> "OpenOrders":
|
|
open_orders_account = AccountInfo.load(context, address)
|
|
if open_orders_account is None:
|
|
raise Exception(f"OpenOrders account not found at address '{address}'")
|
|
return OpenOrders.parse(open_orders_account, base, quote)
|
|
|
|
@staticmethod
|
|
def load_for_market_and_owner(
|
|
context: Context,
|
|
market: PublicKey,
|
|
owner: PublicKey,
|
|
program_address: PublicKey,
|
|
base: Token,
|
|
quote: Token,
|
|
) -> typing.Sequence["OpenOrders"]:
|
|
filters = [
|
|
MemcmpOpts(
|
|
offset=layouts.ACCOUNT_FLAGS.sizeof() + 5, bytes=encode_key(market)
|
|
),
|
|
MemcmpOpts(
|
|
offset=layouts.ACCOUNT_FLAGS.sizeof() + 37, bytes=encode_key(owner)
|
|
),
|
|
]
|
|
|
|
account_infos = AccountInfo.load_by_program(
|
|
context,
|
|
program_address,
|
|
data_size=layouts.OPEN_ORDERS.sizeof(),
|
|
memcmp_opts=filters,
|
|
)
|
|
return list(
|
|
map(
|
|
lambda acc: OpenOrders.parse(acc, base, quote),
|
|
account_infos,
|
|
)
|
|
)
|
|
|
|
def subscribe(
|
|
self,
|
|
context: Context,
|
|
websocketmanager: WebSocketSubscriptionManager,
|
|
callback: typing.Callable[["OpenOrders"], None],
|
|
) -> Disposable:
|
|
def __parser(account_info: AccountInfo) -> OpenOrders:
|
|
return OpenOrders.parse(
|
|
account_info,
|
|
self.base,
|
|
self.quote,
|
|
)
|
|
|
|
subscription = WebSocketAccountSubscription(context, self.address, __parser)
|
|
websocketmanager.add(subscription)
|
|
subscription.publisher.subscribe(on_next=callback) # type: ignore[call-arg]
|
|
|
|
return subscription
|
|
|
|
def __str__(self) -> str:
|
|
placed_orders = "\n ".join(map(str, self.placed_orders)) or "None"
|
|
|
|
return f"""« OpenOrders {self.base.symbol}/{self.quote.symbol} [{self.address}]:
|
|
Flags: {self.account_flags}
|
|
Program ID: {self.program_address}
|
|
Market: {self.market}
|
|
Owner: {self.owner}
|
|
Base Token [{self.base.decimals} decimals]: {self.base_token_free:,.8f} of {self.base_token_total:,.8f}
|
|
Quote Token [{self.quote.decimals} decimals]: {self.quote_token_free:,.8f} of {self.quote_token_total:,.8f}
|
|
Referrer Rebate Accrued: {self.referrer_rebate_accrued:,.8f}
|
|
Orders:
|
|
{placed_orders}
|
|
»"""
|