Added a streaming watcher for some account types.

This commit is contained in:
Geoff Taylor 2021-07-21 16:35:38 +01:00
parent f8e8480085
commit 817b31f8ea
5 changed files with 264 additions and 2 deletions

103
bin/watch-address Executable file
View File

@ -0,0 +1,103 @@
#!/usr/bin/env pyston3
import argparse
import logging
import os
import os.path
import rx
import rx.operators
import sys
import threading
from decimal import Decimal
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
import mango.layouts # nopep8
import mango.marketmaking.fixedratiosdesiredordersbuilder # nopep8
import mango.marketmaking.marketmaker # nopep8
import mango.marketmaking.modelstate # nopep8
import mango.marketmaking.toleranceorderreconciler # nopep8
parser = argparse.ArgumentParser(description="Shows the on-chain data of a particular account.")
mango.Context.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--address", type=PublicKey, required=True, help="Address of the Solana account to watch")
parser.add_argument("--account-type", type=str, required=True,
help="Underlying object type of the data in the AccountInfo")
args = parser.parse_args()
logging.getLogger().setLevel(args.log_level)
logging.warning(mango.WARNING_DISCLAIMER_TEXT)
context = mango.Context.from_command_line_parameters(args)
disposer = mango.DisposePropagator()
manager = mango.WebSocketSubscriptionManager()
disposer.add_disposable(manager)
def build_converter(context: mango.Context, account_type):
account_type_upper = account_type.upper()
if account_type_upper == "ACCOUNTINFO":
return lambda account_info: account_info
elif account_type_upper == "GROUP":
return lambda account_info: mango.Group.parse(context, account_info)
elif account_type_upper == "ACCOUNT":
account_info = mango.AccountInfo.load(context, args.address)
if account_info is None:
raise Exception(f"No account at {args.address}.")
layout_account = mango.layouts.MANGO_ACCOUNT.parse(account_info.data)
group_address = layout_account.group
group: mango.Group = mango.Group.load(context, group_address)
return lambda account_info: mango.Account.parse(context, account_info, group)
elif account_type_upper == "OPENORDERS":
return lambda account_info: mango.OpenOrders.parse(account_info, Decimal(6), Decimal(6))
elif account_type_upper == "PERPEVENTQUEUE":
return lambda account_info: mango.PerpEventQueue.parse(account_info)
elif account_type_upper == "PERPEVENTS":
class EventSplitter:
def __init__(self):
self.seq_num = Decimal(0)
def split(self, item: mango.PerpEventQueue) -> None:
events = item.events[int(self.seq_num):int(item.sequence_number)]
self.seq_num = item.sequence_number
return events
splitter: EventSplitter = EventSplitter()
def _split_events(account_info: mango.AccountInfo):
perp_event_queue = mango.PerpEventQueue.parse(account_info)
return splitter.split(perp_event_queue)
return _split_events
raise Exception(f"Could not find AccountInfo converter for type {account_type}.")
subscription = mango.WebSocketSubscription[mango.AccountInfo](
context, args.address, lambda account_info: account_info)
manager.add(subscription)
subscription.publisher.pipe(
rx.operators.flat_map(build_converter(context, args.account_type))
).subscribe(mango.PrintingObserverSubscriber(False))
websocket_url = context.cluster_url.replace("https", "ws", 1)
ws: mango.ReconnectingWebsocket = mango.ReconnectingWebsocket(websocket_url, manager.open_handler, manager.on_item)
ws.ping_interval = 10
ws.open()
# Wait - don't exit. Exiting will be handled by signals/interrupts.
waiter = threading.Event()
try:
waiter.wait()
except:
pass
logging.info("Shutting down...")
ws.close()
disposer.dispose()
logging.info("Shutdown complete.")

View File

@ -31,6 +31,7 @@ from .orders import Order, OrderType, Side
from .ownedtokenvalue import OwnedTokenValue
from .oracle import OracleSource, Price, Oracle, OracleProvider
from .oraclefactory import create_oracle_provider
from .perpeventqueue import Event, PerpEventQueue
from .perpmarket import PerpMarket
from .perpmarketinfo import PerpMarketInfo
from .perpmarketinstructionbuilder import PerpMarketInstructionBuilder

View File

@ -942,7 +942,7 @@ OUT_EVENT = construct.Struct(
assert OUT_EVENT.sizeof() == _EVENT_SIZE
UNKNOWN_EVENT = construct.Struct(
"flags" / construct.Bytes(1),
"event_type" / construct.Bytes(1),
construct.Padding(7),
"owner" / PublicKeyAdapter(),
construct.Padding(_EVENT_SIZE - 40)

158
mango/perpeventqueue.py Normal file
View File

@ -0,0 +1,158 @@
# # ⚠ 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 datetime import datetime
from decimal import Decimal
from solana.publickey import PublicKey
from .accountinfo import AccountInfo
from .addressableaccount import AddressableAccount
from .context import Context
from .layouts import layouts
from .metadata import Metadata
from .orders import Side
from .version import Version
# # 🥭 PerpEventQueue class
#
# `PerpEventQueue` stores details of how to reach `PerpEventQueue```.
#
class Event:
def __init__(self, event_type: int):
self.event_type: int = event_type
def __repr__(self) -> str:
return f"{self}"
class FillEvent(Event):
def __init__(self, event_type: int, timestamp: datetime, side: Side, price: Decimal, quantity: Decimal,
best_initial: Decimal, maker_slot: Decimal, maker_out: bool,
maker: PublicKey, maker_order_id: Decimal, maker_client_order_id: Decimal,
taker: PublicKey, taker_order_id: Decimal, taker_client_order_id: Decimal):
super().__init__(event_type)
self.timestamp: datetime = timestamp
self.side: Side = side
self.price: Decimal = price
self.quantity: Decimal = quantity
self.best_initial: Decimal = best_initial
self.maker_slot: Decimal = maker_slot
self.maker_out: bool = maker_out
self.maker: PublicKey = maker
self.maker_order_id: Decimal = maker_order_id
self.maker_client_order_id: Decimal = maker_client_order_id
self.taker: PublicKey = taker
self.taker_order_id: Decimal = taker_order_id
self.taker_client_order_id: Decimal = taker_client_order_id
def __str__(self):
return f"""« 𝙵𝚒𝚕𝚕𝙴𝚟𝚎𝚗𝚝 [{self.timestamp}] {self.side} {self.quantity} at {self.price}
Maker: {self.maker}, {self.maker_order_id} / {self.maker_client_order_id}
Taker: {self.taker}, {self.taker_order_id} / {self.taker_client_order_id}
Best Initial: {self.best_initial}
Maker Slot: {self.maker_slot}
Maker Out: {self.maker_out}
»"""
class OutEvent(Event):
def __init__(self, event_type: int, owner: PublicKey, side: Side, quantity: Decimal, slot: Decimal):
super().__init__(event_type)
self.owner: PublicKey = owner
self.side: Side = side
self.slot: Decimal = slot
self.quantity: Decimal = quantity
def __str__(self):
return f"""« 𝙾𝚞𝚝𝙴𝚟𝚎𝚗𝚝 [{self.owner}] {self.side} {self.quantity}, slot: {self.slot} »"""
class UnknownEvent(Event):
def __init__(self, event_type: int, owner: PublicKey):
super().__init__(event_type)
self.owner: PublicKey = owner
def __str__(self):
return f"« 𝚄𝚗𝚔𝚗𝚘𝚠𝚗𝙴𝚟𝚎𝚗𝚝 [{self.owner}] »"
def event_builder(event_layout) -> Event:
if event_layout.event_type == b'\x00':
return FillEvent(event_layout.event_type, event_layout.timestamp, event_layout.side,
event_layout.price, event_layout.quantity, event_layout.best_initial,
event_layout.maker_slot, event_layout.maker_out, event_layout.maker,
event_layout.maker_order_id, event_layout.maker_client_order_id,
event_layout.taker, event_layout.taker_order_id,
event_layout.taker_client_order_id)
elif event_layout.event_type == b'\x01':
return OutEvent(event_layout.event_type, event_layout.owner, event_layout.side, event_layout.quantity, event_layout.slot)
else:
return UnknownEvent(event_layout.event_type, event_layout.owner)
class PerpEventQueue(AddressableAccount):
def __init__(self, account_info: AccountInfo, version: Version, meta_data: Metadata,
head: Decimal, count: Decimal, sequence_number: Decimal, events: typing.Sequence[Event]):
super().__init__(account_info)
self.version: Version = version
self.meta_data: Metadata = meta_data
self.head: Decimal = head
self.count: Decimal = count
self.sequence_number: Decimal = sequence_number
self.events: typing.Sequence[Event] = events
@staticmethod
def from_layout(layout: layouts.PERP_EVENT_QUEUE, account_info: AccountInfo, version: Version) -> "PerpEventQueue":
meta_data: Metadata = layout.meta_data
head: Decimal = layout.head
count: Decimal = layout.count
seq_num: Decimal = layout.seq_num
events: typing.Sequence[Event] = list(map(event_builder, layout.events))
return PerpEventQueue(account_info, version, meta_data, head, count, seq_num, events)
@staticmethod
def parse(account_info: AccountInfo) -> "PerpEventQueue":
# Data length isn't fixed so can't check we get the right value the way we normally do.
layout = layouts.PERP_EVENT_QUEUE.parse(account_info.data)
return PerpEventQueue.from_layout(layout, account_info, Version.V1)
@staticmethod
def load(context: Context, address: PublicKey) -> "PerpEventQueue":
account_info = AccountInfo.load(context, address)
if account_info is None:
raise Exception(f"PerpEventQueue account not found at address '{address}'")
return PerpEventQueue.parse(account_info)
def __str__(self):
events = "\n ".join([f"{event}".replace("\n", "\n ")
for event in self.events if event is not None]) or "None"
return f"""« 𝙿𝚎𝚛𝚙𝙴𝚟𝚎𝚗𝚝𝚀𝚞𝚎𝚞𝚎 [{self.version}] {self.address}
{self.meta_data}
Head: {self.head}
Count: {self.count}
Sequence Number: {self.sequence_number}
Events:
{events}
»"""

View File

@ -29,7 +29,7 @@ from .version import Version
# # 🥭 NodeBank class
#
# `NodeBank` stores details of how to reach `NodeBank```.
# `NodeBank` stores details of deposits/borrows and vault.
#