Better logging of failing instructions.

This commit is contained in:
Geoff Taylor 2021-08-04 17:50:38 +01:00
parent cc6f62c312
commit 7033a080bf
7 changed files with 87 additions and 23 deletions

View File

@ -96,7 +96,9 @@ class Account(AddressableAccount):
def __init__(self, account_info: AccountInfo, version: Version, def __init__(self, account_info: AccountInfo, version: Version,
meta_data: Metadata, group: Group, owner: PublicKey, meta_data: Metadata, group: Group, owner: PublicKey,
shared_quote_token: AccountBasketToken, shared_quote_token: AccountBasketToken,
basket_indices: typing.Sequence[bool], basket: typing.Sequence[AccountBasketBaseToken], in_margin_basket: typing.Sequence[bool],
basket_indices: typing.Sequence[bool],
basket: typing.Sequence[AccountBasketBaseToken],
msrm_amount: Decimal, being_liquidated: bool, is_bankrupt: bool): msrm_amount: Decimal, being_liquidated: bool, is_bankrupt: bool):
super().__init__(account_info) super().__init__(account_info)
self.version: Version = version self.version: Version = version
@ -105,6 +107,7 @@ class Account(AddressableAccount):
self.group: Group = group self.group: Group = group
self.owner: PublicKey = owner self.owner: PublicKey = owner
self.shared_quote_token: AccountBasketToken = shared_quote_token self.shared_quote_token: AccountBasketToken = shared_quote_token
self.in_margin_basket: typing.Sequence[bool] = in_margin_basket
self.basket_indices: typing.Sequence[bool] = basket_indices self.basket_indices: typing.Sequence[bool] = basket_indices
self.basket: typing.Sequence[AccountBasketBaseToken] = basket self.basket: typing.Sequence[AccountBasketBaseToken] = basket
self.msrm_amount: Decimal = msrm_amount self.msrm_amount: Decimal = msrm_amount
@ -116,9 +119,10 @@ class Account(AddressableAccount):
meta_data = Metadata.from_layout(layout.meta_data) meta_data = Metadata.from_layout(layout.meta_data)
owner: PublicKey = layout.owner owner: PublicKey = layout.owner
in_margin_basket: typing.Sequence[bool] = list([bool(in_basket) for in_basket in layout.in_margin_basket]) in_margin_basket: typing.Sequence[bool] = list([bool(in_basket) for in_basket in layout.in_margin_basket])
active_in_basket: typing.List[bool] = []
basket: typing.List[AccountBasketBaseToken] = [] basket: typing.List[AccountBasketBaseToken] = []
for index, token_info in enumerate(group.tokens[:-1]): for index, token_info in enumerate(group.tokens[:-1]):
if token_info and in_margin_basket[index]: if token_info:
intrinsic_deposit = token_info.root_bank.deposit_index * layout.deposits[index] intrinsic_deposit = token_info.root_bank.deposit_index * layout.deposits[index]
deposit = TokenValue(token_info.token, token_info.token.shift_to_decimals(intrinsic_deposit)) deposit = TokenValue(token_info.token, token_info.token.shift_to_decimals(intrinsic_deposit))
intrinsic_borrow = token_info.root_bank.borrow_index * layout.borrows[index] intrinsic_borrow = token_info.root_bank.borrow_index * layout.borrows[index]
@ -128,6 +132,9 @@ class Account(AddressableAccount):
basket_item: AccountBasketBaseToken = AccountBasketBaseToken( basket_item: AccountBasketBaseToken = AccountBasketBaseToken(
token_info, deposit, borrow, spot_open_orders, perp_account) token_info, deposit, borrow, spot_open_orders, perp_account)
basket += [basket_item] basket += [basket_item]
active_in_basket += [True]
else:
active_in_basket += [False]
quote_token_info: typing.Optional[TokenInfo] = group.tokens[-1] quote_token_info: typing.Optional[TokenInfo] = group.tokens[-1]
if quote_token_info is None: if quote_token_info is None:
@ -145,7 +152,7 @@ class Account(AddressableAccount):
being_liquidated: bool = bool(layout.being_liquidated) being_liquidated: bool = bool(layout.being_liquidated)
is_bankrupt: bool = bool(layout.is_bankrupt) is_bankrupt: bool = bool(layout.is_bankrupt)
return Account(account_info, version, meta_data, group, owner, quote, in_margin_basket, basket, msrm_amount, being_liquidated, is_bankrupt) return Account(account_info, version, meta_data, group, owner, quote, in_margin_basket, active_in_basket, basket, msrm_amount, being_liquidated, is_bankrupt)
@staticmethod @staticmethod
def parse(account_info: AccountInfo, group: Group) -> "Account": def parse(account_info: AccountInfo, group: Group) -> "Account":

View File

@ -16,19 +16,42 @@
import logging import logging
import typing import typing
from pyserum._layouts.instructions import InstructionType as SerumInstructionType
from solana.account import Account as SolanaAccount from solana.account import Account as SolanaAccount
from solana.blockhash import Blockhash from solana.blockhash import Blockhash
from solana.publickey import PublicKey from solana.publickey import PublicKey
from solana.transaction import Transaction, TransactionInstruction from solana.transaction import Transaction, TransactionInstruction
from .context import Context from .context import Context
from .layouts import layouts
from .transactionscout import MangoInstruction, InstructionType
from .wallet import Wallet from .wallet import Wallet
_MAXIMUM_TRANSACTION_LENGTH = 1280 - 40 - 8 _MAXIMUM_TRANSACTION_LENGTH = 1280 - 40 - 8
_SIGNATURE_LENGTH = 64 _SIGNATURE_LENGTH = 64
def _instruction_to_str(instruction: TransactionInstruction) -> str: def _mango_instruction_to_str(instruction: TransactionInstruction) -> str:
initial = layouts.MANGO_INSTRUCTION_VARIANT_FINDER.parse(instruction.data)
parser = layouts.InstructionParsersByVariant[initial.variant]
if parser is None:
raise Exception(
f"Could not find instruction parser for variant {initial.variant} / {InstructionType(initial.variant)}.")
accounts: typing.List[PublicKey] = list(map(lambda meta: meta.pubkey, instruction.keys))
parsed = parser.parse(instruction.data)
instruction_type = InstructionType(int(parsed.variant))
return str(MangoInstruction(instruction_type, parsed, accounts))
def _serum_instruction_to_str(instruction: TransactionInstruction) -> str:
initial = layouts.SERUM_INSTRUCTION_VARIANT_FINDER.parse(instruction.data)
instruction_type = SerumInstructionType(initial.variant)
return f"« Serum Instruction: {instruction_type.name}: " + "".join("{:02x}".format(x) for x in instruction.data) + "»"
def _raw_instruction_to_str(instruction: TransactionInstruction) -> str:
report: typing.List[str] = [] report: typing.List[str] = []
for index, key in enumerate(instruction.keys): for index, key in enumerate(instruction.keys):
report += [f"Key[{index}]: {key.pubkey} {key.is_signer: <5} {key.is_writable: <5}"] report += [f"Key[{index}]: {key.pubkey} {key.is_signer: <5} {key.is_writable: <5}"]
@ -37,6 +60,14 @@ def _instruction_to_str(instruction: TransactionInstruction) -> str:
return "\n".join(report) return "\n".join(report)
def _instruction_to_str(context: Context, instruction: TransactionInstruction) -> str:
if instruction.program_id == context.program_id:
return _mango_instruction_to_str(instruction)
elif instruction.program_id == context.dex_program_id:
return _serum_instruction_to_str(instruction)
return _raw_instruction_to_str(instruction)
def _split_instructions_into_chunks(signers: typing.Sequence[SolanaAccount], instructions: typing.Sequence[TransactionInstruction]) -> typing.Sequence[typing.Sequence[TransactionInstruction]]: def _split_instructions_into_chunks(signers: typing.Sequence[SolanaAccount], instructions: typing.Sequence[TransactionInstruction]) -> typing.Sequence[typing.Sequence[TransactionInstruction]]:
vetted_chunks: typing.List[typing.List[TransactionInstruction]] = [] vetted_chunks: typing.List[typing.List[TransactionInstruction]] = []
current_chunk: typing.List[TransactionInstruction] = [] current_chunk: typing.List[TransactionInstruction] = []
@ -151,7 +182,7 @@ class CombinableInstructions():
response = context.client.send_transaction(transaction, *self.signers, opts=context.transaction_options) response = context.client.send_transaction(transaction, *self.signers, opts=context.transaction_options)
results += [context.unwrap_or_raise_exception(response)] results += [context.unwrap_or_raise_exception(response)]
except Exception as exception: except Exception as exception:
instruction_str: str = _instruction_to_str(instruction) instruction_str: str = _instruction_to_str(context, instruction)
self.logger.error(f"""Error executing individual instruction: {exception} self.logger.error(f"""Error executing individual instruction: {exception}
{instruction_str}""") {instruction_str}""")
@ -175,10 +206,12 @@ class CombinableInstructions():
try: try:
response = context.client.send_transaction(transaction, *self.signers, opts=context.transaction_options) response = context.client.send_transaction(transaction, *self.signers, opts=context.transaction_options)
results += [context.unwrap_or_raise_exception(response)] results += [context.unwrap_or_raise_exception(response)]
except: except Exception as exception:
starts_at = sum(len(ch) for ch in chunks[0:index]) starts_at = sum(len(ch) for ch in chunks[0:index])
instruction_text = list(map(_instruction_to_str, chunk)) instruction_text = "\n".join(list(map(lambda ins: _instruction_to_str(context, ins), chunk)))
self.logger.error(f"""Error executing chunk {index} (instructions {starts_at} to {starts_at + len(chunk)}) of CombinableInstruction. Failing instruction(s): self.logger.error(f"""Error executing chunk {index} (instructions {starts_at} to {starts_at + len(chunk)}) of CombinableInstruction.
Exception: {exception}
Failing instruction(s):
{instruction_text}""") {instruction_text}""")
return results return results
@ -192,7 +225,7 @@ class CombinableInstructions():
report += [f"Signer[{index}]: {signer.public_key()}"] report += [f"Signer[{index}]: {signer.public_key()}"]
for instruction in self.instructions: for instruction in self.instructions:
report += _instruction_to_str(instruction) report += _raw_instruction_to_str(instruction)
return "\n".join(report) return "\n".join(report)

View File

@ -77,12 +77,8 @@ class InventoryAccountWatcher:
self.account_watcher: Watcher[Account] = account_watcher self.account_watcher: Watcher[Account] = account_watcher
account: Account = account_watcher.latest account: Account = account_watcher.latest
base_value = TokenValue.find_by_symbol(account.net_assets, market.base.symbol) base_value = TokenValue.find_by_symbol(account.net_assets, market.base.symbol)
if base_value is None:
raise Exception(f"Could not find net assets in account {account.address} for base token {market.base}.")
self.base_index: int = account.net_assets.index(base_value) self.base_index: int = account.net_assets.index(base_value)
quote_value = TokenValue.find_by_symbol(account.net_assets, market.quote.symbol) quote_value = TokenValue.find_by_symbol(account.net_assets, market.quote.symbol)
if quote_value is None:
raise Exception(f"Could not find net assets in account {account.address} for quote token {market.quote}.")
self.quote_index: int = account.net_assets.index(quote_value) self.quote_index: int = account.net_assets.index(quote_value)
@property @property

View File

@ -1101,6 +1101,12 @@ MANGO_INSTRUCTION_VARIANT_FINDER = construct.Struct(
"variant" / construct.BytesInteger(4, swapped=True) "variant" / construct.BytesInteger(4, swapped=True)
) )
SERUM_INSTRUCTION_VARIANT_FINDER = construct.Struct(
"version" / construct.BytesInteger(1, swapped=True),
"variant" / construct.BytesInteger(4, swapped=True)
)
# /// Place an order on a perp market # /// Place an order on a perp market
# /// Accounts expected by this instruction (6): # /// Accounts expected by this instruction (6):
# /// 0. `[]` mango_group_ai - TODO # /// 0. `[]` mango_group_ai - TODO

View File

@ -41,6 +41,11 @@ class Side(enum.Enum):
BUY = "BUY" BUY = "BUY"
SELL = "SELL" SELL = "SELL"
@staticmethod
def from_value(value: Decimal) -> "Side":
converted: pyserum.enums.Side = pyserum.enums.Side(int(value))
return Side.BUY if converted == pyserum.enums.Side.BUY else Side.SELL
def __str__(self) -> str: def __str__(self) -> str:
return self.value return self.value
@ -63,6 +68,17 @@ class OrderType(enum.Enum):
IOC = "IOC" IOC = "IOC"
POST_ONLY = "POST_ONLY" POST_ONLY = "POST_ONLY"
@staticmethod
def from_value(value: Decimal) -> "OrderType":
converted: pyserum.enums.OrderType = pyserum.enums.OrderType(int(value))
if converted == pyserum.enums.OrderType.IOC:
return OrderType.IOC
elif converted == pyserum.enums.OrderType.POST_ONLY:
return OrderType.POST_ONLY
elif converted == pyserum.enums.OrderType.LIMIT:
return OrderType.LIMIT
return OrderType.UNKNOWN
def __str__(self) -> str: def __str__(self) -> str:
return self.value return self.value
@ -99,7 +115,7 @@ class Order(typing.NamedTuple):
def from_serum_order(serum_order: SerumOrder) -> "Order": def from_serum_order(serum_order: SerumOrder) -> "Order":
price = Decimal(serum_order.info.price) price = Decimal(serum_order.info.price)
quantity = Decimal(serum_order.info.size) quantity = Decimal(serum_order.info.size)
side = Side.BUY if serum_order.side == pyserum.enums.Side.BUY else Side.SELL side = Side.from_value(serum_order.side)
order = Order(id=serum_order.order_id, side=side, price=price, quantity=quantity, order = Order(id=serum_order.order_id, side=side, price=price, quantity=quantity,
client_id=serum_order.client_id, owner=serum_order.open_order_address, client_id=serum_order.client_id, owner=serum_order.open_order_address,
order_type=OrderType.UNKNOWN) order_type=OrderType.UNKNOWN)

View File

@ -26,6 +26,7 @@ from solana.publickey import PublicKey
from .context import Context from .context import Context
from .instructiontype import InstructionType from .instructiontype import InstructionType
from .layouts import layouts from .layouts import layouts
from .orders import OrderType, Side
from .ownedtokenvalue import OwnedTokenValue from .ownedtokenvalue import OwnedTokenValue
from .tokenvalue import TokenValue from .tokenvalue import TokenValue
@ -182,8 +183,6 @@ _target_indices: typing.Dict[InstructionType, int] = {
# This class packages up Mango instruction data, which can come from disparate parts of the # This class packages up Mango instruction data, which can come from disparate parts of the
# transaction. Keeping it all together here makes many things simpler. # transaction. Keeping it all together here makes many things simpler.
# #
class MangoInstruction: class MangoInstruction:
def __init__(self, instruction_type: InstructionType, instruction_data: typing.Any, accounts: typing.Sequence[PublicKey]): def __init__(self, instruction_type: InstructionType, instruction_data: typing.Any, accounts: typing.Sequence[PublicKey]):
self.instruction_type = instruction_type self.instruction_type = instruction_type
@ -245,17 +244,17 @@ class MangoInstruction:
elif instruction_type == InstructionType.CacheRootBanks: elif instruction_type == InstructionType.CacheRootBanks:
pass pass
elif instruction_type == InstructionType.PlaceSpotOrder: elif instruction_type == InstructionType.PlaceSpotOrder:
additional_data = f"side: {self.instruction_data.side}, order_type: {self.instruction_data.order_type}, limit_price: {self.instruction_data.limit_price}, max_base_quantity: {self.instruction_data.max_base_quantity}, max_quote_quantity: {self.instruction_data.max_quote_quantity}, self_trade_behavior: {self.instruction_data.self_trade_behavior}, client_id: {self.instruction_data.client_id}, limit: {self.instruction_data.limit}" additional_data = f"side: {Side.from_value(self.instruction_data.side)}, order_type: {OrderType.from_value(self.instruction_data.order_type)}, limit_price: {self.instruction_data.limit_price}, max_base_quantity: {self.instruction_data.max_base_quantity}, max_quote_quantity: {self.instruction_data.max_quote_quantity}, self_trade_behavior: {self.instruction_data.self_trade_behavior}, client_id: {self.instruction_data.client_id}, limit: {self.instruction_data.limit}"
elif instruction_type == InstructionType.AddOracle: elif instruction_type == InstructionType.AddOracle:
pass pass
elif instruction_type == InstructionType.AddPerpMarket: elif instruction_type == InstructionType.AddPerpMarket:
pass pass
elif instruction_type == InstructionType.PlacePerpOrder: elif instruction_type == InstructionType.PlacePerpOrder:
additional_data = f"side: {self.instruction_data.side}, order_type: {self.instruction_data.order_type}, price: {self.instruction_data.price}, quantity: {self.instruction_data.quantity}, client_order_id: {self.instruction_data.client_order_id}" additional_data = f"side: {Side.from_value(self.instruction_data.side)}, order_type: {OrderType.from_value(self.instruction_data.order_type)}, price: {self.instruction_data.price}, quantity: {self.instruction_data.quantity}, client_order_id: {self.instruction_data.client_order_id}"
elif instruction_type == InstructionType.CancelPerpOrderByClientId: elif instruction_type == InstructionType.CancelPerpOrderByClientId:
additional_data = f"client ID: {self.instruction_data.client_order_id}" additional_data = f"client ID: {self.instruction_data.client_order_id}"
elif instruction_type == InstructionType.CancelPerpOrder: elif instruction_type == InstructionType.CancelPerpOrder:
additional_data = f"order ID: {self.instruction_data.order_id}, side: {self.instruction_data.side}" additional_data = f"order ID: {self.instruction_data.order_id}, side: {Side.from_value(self.instruction_data.side)}"
elif instruction_type == InstructionType.ConsumeEvents: elif instruction_type == InstructionType.ConsumeEvents:
additional_data = f"limit: {self.instruction_data.limit}" additional_data = f"limit: {self.instruction_data.limit}"
elif instruction_type == InstructionType.CachePerpMarkets: elif instruction_type == InstructionType.CachePerpMarkets:
@ -267,7 +266,7 @@ class MangoInstruction:
elif instruction_type == InstructionType.SettleFunds: elif instruction_type == InstructionType.SettleFunds:
pass pass
elif instruction_type == InstructionType.CancelSpotOrder: elif instruction_type == InstructionType.CancelSpotOrder:
additional_data = f"order ID: {self.instruction_data.order_id}, side: {self.instruction_data.side}" additional_data = f"order ID: {self.instruction_data.order_id}, side: {Side.from_value(self.instruction_data.side)}"
elif instruction_type == InstructionType.UpdateRootBank: elif instruction_type == InstructionType.UpdateRootBank:
pass pass
elif instruction_type == InstructionType.SettlePnl: elif instruction_type == InstructionType.SettlePnl:
@ -310,6 +309,10 @@ class MangoInstruction:
return MangoInstruction(instruction_type, parsed, accounts) return MangoInstruction(instruction_type, parsed, accounts)
@staticmethod
def _order_type_enum(side: Decimal):
return Side.BUY if side == Decimal(0) else Side.SELL
def __str__(self) -> str: def __str__(self) -> str:
parameters = self.describe_parameters() or "None" parameters = self.describe_parameters() or "None"
return f"« {self.instruction_type.name}: {parameters} »" return f"« {self.instruction_type.name}: {parameters} »"

View File

@ -10,7 +10,8 @@ def test_construction():
meta_data = mango.Metadata(layouts.DATA_TYPE.Group, mango.Version.V1, True) meta_data = mango.Metadata(layouts.DATA_TYPE.Group, mango.Version.V1, True)
group = fake_seeded_public_key("group") group = fake_seeded_public_key("group")
owner = fake_seeded_public_key("owner") owner = fake_seeded_public_key("owner")
in_margin_basket = [False, True, False, True, True] in_margin_basket = [False, False, False, False, False]
active_in_basket = [False, True, False, True, True]
quote_deposit = fake_token_value(Decimal(50)) quote_deposit = fake_token_value(Decimal(50))
quote_borrow = fake_token_value(Decimal(5)) quote_borrow = fake_token_value(Decimal(5))
quote = mango.AccountBasketToken(fake_token_info(), quote_deposit, quote_borrow) quote = mango.AccountBasketToken(fake_token_info(), quote_deposit, quote_borrow)
@ -33,7 +34,8 @@ def test_construction():
is_bankrupt = False is_bankrupt = False
actual = mango.Account(account_info, mango.Version.V1, meta_data, group, owner, quote, actual = mango.Account(account_info, mango.Version.V1, meta_data, group, owner, quote,
in_margin_basket, basket, msrm_amount, being_liquidated, is_bankrupt) in_margin_basket, active_in_basket, basket, msrm_amount, being_liquidated,
is_bankrupt)
assert actual is not None assert actual is not None
assert actual.logger is not None assert actual.logger is not None
@ -41,7 +43,8 @@ def test_construction():
assert actual.meta_data == meta_data assert actual.meta_data == meta_data
assert actual.group == group assert actual.group == group
assert actual.owner == owner assert actual.owner == owner
assert actual.basket_indices == in_margin_basket assert actual.basket_indices == active_in_basket
assert actual.in_margin_basket == in_margin_basket
assert actual.deposits == [None, deposit1, None, deposit2, deposit3, quote_deposit] assert actual.deposits == [None, deposit1, None, deposit2, deposit3, quote_deposit]
assert actual.borrows == [None, borrow1, None, borrow2, borrow3, quote_borrow] assert actual.borrows == [None, borrow1, None, borrow2, borrow3, quote_borrow]
assert actual.net_assets == [None, deposit1 - borrow1, None, assert actual.net_assets == [None, deposit1 - borrow1, None,