Improved transaction exception reporting.

This commit is contained in:
Geoff Taylor 2021-08-19 09:46:54 +01:00
parent a51b38049b
commit a96b67e729
10 changed files with 501 additions and 385 deletions

View File

@ -22,11 +22,13 @@ from .idsjsontokenlookup import IdsJsonTokenLookup
from .idsjsonmarketlookup import IdsJsonMarketLookup
from .inventory import Inventory, InventoryAccountWatcher, spl_token_inventory_loader, account_inventory_loader
from .instructions import build_create_solana_account_instructions, build_create_spl_account_instructions, build_create_associated_spl_account_instructions, build_transfer_spl_tokens_instructions, build_close_spl_account_instructions, build_create_serum_open_orders_instructions, build_serum_place_order_instructions, build_serum_consume_events_instructions, build_serum_settle_instructions, build_spot_place_order_instructions, build_cancel_spot_order_instructions, build_cancel_perp_order_instructions, build_mango_consume_events_instructions, build_create_account_instructions, build_place_perp_order_instructions, build_deposit_instructions, build_withdraw_instructions, build_redeem_accrued_mango_instructions
from .instructionreporter import InstructionReporter, SerumInstructionReporter, MangoInstructionReporter, CompoundInstructionReporter
from .instructiontype import InstructionType
from .liquidatablereport import LiquidatableState, LiquidatableReport
from .liquidationevent import LiquidationEvent
from .liquidationprocessor import LiquidationProcessor, LiquidationProcessorState
from .lotsizeconverter import LotSizeConverter, NullLotSizeConverter
from .mangoinstruction import MangoInstruction
from .market import InventorySource, Market
from .marketinstructionbuilder import MarketInstructionBuilder, NullMarketInstructionBuilder
from .marketlookup import MarketLookup, NullMarketLookup, CompoundMarketLookup
@ -69,7 +71,7 @@ from .tokeninfo import TokenInfo
from .tokenlookup import TokenLookup, NullTokenLookup, CompoundTokenLookup
from .tokenvalue import TokenValue
from .tradeexecutor import TradeExecutor, NullTradeExecutor, ImmediateTradeExecutor
from .transactionscout import MangoInstruction, TransactionScout, fetch_all_recent_transaction_signatures
from .transactionscout import TransactionScout, fetch_all_recent_transaction_signatures, mango_instruction_from_response
from .version import Version
from .wallet import Wallet
from .walletbalancer import TargetBalance, FixedTargetBalance, PercentageTargetBalance, TargetBalanceParser, sort_changes_for_trades, calculate_required_balance_changes, FilterSmallChanges, WalletBalancer, NullWalletBalancer, LiveWalletBalancer

View File

@ -32,6 +32,7 @@ from solana.rpc.commitment import Commitment
from solana.rpc.types import DataSliceOpts, MemcmpOpts, RPCResponse, TokenAccountOpts, TxOpts
from .constants import SOL_DECIMAL_DIVISOR
from .instructionreporter import InstructionReporter
# # 🥭 RateLimitException class
@ -67,8 +68,9 @@ class TooManyRequestsRateLimitException(RateLimitException):
# of problems at the right place.
#
class TransactionException(Exception):
def __init__(self, message: str, code: int, name: str, accounts: typing.Union[str, typing.List[str], None], errors: typing.Union[str, typing.List[str], None], logs: typing.Union[str, typing.List[str], None]):
def __init__(self, transaction: typing.Optional[Transaction], message: str, code: int, name: str, accounts: typing.Union[str, typing.List[str], None], errors: typing.Union[str, typing.List[str], None], logs: typing.Union[str, typing.List[str], None], instruction_reporter: InstructionReporter = InstructionReporter()):
super().__init__(message)
self.transaction: typing.Optional[Transaction] = transaction
self.message: str = message
self.code: int = code
self.name: str = name
@ -84,8 +86,13 @@ class TransactionException(Exception):
self.accounts: typing.List[str] = _ensure_list(accounts)
self.errors: typing.List[str] = _ensure_list(errors)
self.logs: typing.List[str] = _ensure_list(logs)
self.instruction_reporter: InstructionReporter = instruction_reporter
def __str__(self) -> str:
transaction_details = ""
if self.transaction is not None:
instruction_details = "\n".join(list(map(self.instruction_reporter.report, self.transaction.instructions)))
transaction_details = "\n Instructions:\n " + instruction_details.replace("\n", "\n ")
accounts = "No Accounts"
if len(self.accounts) > 0:
accounts = "\n ".join([f"{item}".replace("\n", "\n ") for item in self.accounts])
@ -95,7 +102,7 @@ class TransactionException(Exception):
logs = "No Logs"
if len(self.logs) > 0:
logs = "\n ".join([f"{item}".replace("\n", "\n ") for item in self.logs])
return f"""« 𝚃𝚛𝚊𝚗𝚜𝚊𝚌𝚝𝚒𝚘𝚗𝙴𝚡𝚌𝚎𝚙𝚝𝚒𝚘𝚗 [{self.name}] {self.code}: {self.message}
return f"""« 𝚃𝚛𝚊𝚗𝚜𝚊𝚌𝚝𝚒𝚘𝚗𝙴𝚡𝚌𝚎𝚙𝚝𝚒𝚘𝚗 [{self.name}] {self.code}: {self.message}{transaction_details}
Accounts:
{accounts}
Errors:
@ -127,16 +134,16 @@ UnspecifiedEncoding = "unspecified"
# some common operations better from our point of view.
#
class CompatibleClient:
def __init__(self, name: str, cluster: str, cluster_url: str, commitment: Commitment, skip_preflight: bool):
def __init__(self, name: str, cluster: str, cluster_url: str, commitment: Commitment, skip_preflight: bool, instruction_reporter: InstructionReporter):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.name: str = name
self.cluster: str = cluster
self.cluster_url: str = cluster_url
self._request_counter = itertools.count()
self.commitment: Commitment = commitment
self.skip_preflight: bool = skip_preflight
self.instruction_reporter: InstructionReporter = instruction_reporter
self._request_counter = itertools.count()
self.encoding: str = "base64"
def is_node_healthy(self) -> bool:
@ -250,6 +257,7 @@ class CompatibleClient:
skip_preflight: bool = opts.skip_preflight or self.skip_preflight
try:
return self._send_request(
"sendTransaction",
encoded_transaction,
@ -259,6 +267,11 @@ class CompatibleClient:
_EncodingKey: self.encoding,
}
)
except TransactionException as transaction_exception:
raise TransactionException(transaction, transaction_exception.message, transaction_exception.code,
transaction_exception.name, transaction_exception.accounts,
transaction_exception.errors, transaction_exception.logs,
self.instruction_reporter) from None
def _send_request(self, method: str, *params: typing.Any) -> RPCResponse:
request_id = next(self._request_counter) + 1
@ -293,7 +306,7 @@ class CompatibleClient:
error_accounts = error_data["accounts"] if "accounts" in error_data else "No accounts"
error_err = error_data["err"] if "err" in error_data else "No error text returned"
error_logs = error_data["logs"] if "logs" in error_data else "No logs"
raise TransactionException(exception_message, error_code, self.name,
raise TransactionException(None, exception_message, error_code, self.name,
error_accounts, error_err, error_logs)
# The call succeeded.
@ -377,9 +390,17 @@ class BetterClient:
def skip_preflight(self, value: bool) -> None:
self.compatible_client.skip_preflight = value
@property
def instruction_reporter(self) -> InstructionReporter:
return self.compatible_client.instruction_reporter
@instruction_reporter.setter
def instruction_reporter(self, value: InstructionReporter) -> None:
self.compatible_client.instruction_reporter = value
@staticmethod
def from_configuration(name: str, cluster: str, cluster_url: str, commitment: Commitment, skip_preflight: bool) -> "BetterClient":
compatible = CompatibleClient(name, cluster, cluster_url, commitment, skip_preflight)
def from_configuration(name: str, cluster: str, cluster_url: str, commitment: Commitment, skip_preflight: bool, instruction_reporter: InstructionReporter) -> "BetterClient":
compatible = CompatibleClient(name, cluster, cluster_url, commitment, skip_preflight, instruction_reporter)
return BetterClient(compatible)
def is_node_healthy(self) -> bool:

View File

@ -16,58 +16,19 @@
import logging
import typing
from pyserum._layouts.instructions import InstructionType as SerumInstructionType
from solana.account import Account as SolanaAccount
from solana.blockhash import Blockhash
from solana.publickey import PublicKey
from solana.transaction import Transaction, TransactionInstruction
from .context import Context
from .layouts import layouts
from .transactionscout import MangoInstruction, InstructionType
from .instructionreporter import InstructionReporter
from .wallet import Wallet
_MAXIMUM_TRANSACTION_LENGTH = 1280 - 40 - 8
_SIGNATURE_LENGTH = 64
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] = []
for index, key in enumerate(instruction.keys):
report += [f"Key[{index}]: {key.pubkey} {key.is_signer: <5} {key.is_writable: <5}"]
report += [f"Program ID: {instruction.program_id}"]
report += ["Data: " + "".join("{:02x}".format(x) for x in instruction.data)]
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]]:
vetted_chunks: typing.List[typing.List[TransactionInstruction]] = []
current_chunk: typing.List[TransactionInstruction] = []
@ -173,12 +134,10 @@ class CombinableInstructions():
results += [response]
except Exception as exception:
starts_at = sum(len(ch) for ch in chunks[0:index])
instruction_text = "\n".join(list(map(lambda ins: _instruction_to_str(context, ins), chunk)))
if on_exception_continue:
self.logger.error(f"""[{context.name}] Error executing chunk {index} (instructions {starts_at} to {starts_at + len(chunk)}) of CombinableInstruction.
Exception: {exception}
Failing instruction(s):
{instruction_text}""")
if not on_exception_continue:
{exception}""")
else:
raise exception
return results
@ -188,8 +147,9 @@ Failing instruction(s):
for index, signer in enumerate(self.signers):
report += [f"Signer[{index}]: {signer.public_key()}"]
instruction_reporter: InstructionReporter = InstructionReporter()
for instruction in self.instructions:
report += [_raw_instruction_to_str(instruction)]
report += [instruction_reporter.report(instruction)]
return "\n".join(report)

View File

@ -25,6 +25,7 @@ from solana.rpc.commitment import Commitment
from .client import BetterClient
from .constants import MangoConstants
from .instructionreporter import InstructionReporter, CompoundInstructionReporter
from .marketlookup import MarketLookup
from .tokenlookup import TokenLookup
@ -45,8 +46,9 @@ class Context:
group_name: str, group_id: PublicKey, token_lookup: TokenLookup, market_lookup: MarketLookup):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.name: str = name
instruction_reporter: InstructionReporter = CompoundInstructionReporter.from_ids(program_id, dex_program_id)
self.client: BetterClient = BetterClient.from_configuration(
name, cluster, cluster_url, Commitment("processed"), skip_preflight)
name, cluster, cluster_url, Commitment("processed"), skip_preflight, instruction_reporter)
self.program_id: PublicKey = program_id
self.dex_program_id: PublicKey = dex_program_id
self.group_name: str = group_name

View File

@ -0,0 +1,114 @@
# # ⚠ 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 pyserum._layouts.instructions import InstructionType as SerumInstructionType
from solana.publickey import PublicKey
from solana.transaction import TransactionInstruction
from .instructiontype import InstructionType
from .layouts import layouts
from .mangoinstruction import MangoInstruction
# # 🥭 InstructionReporter class
#
# The `InstructionReporter` class tries to load and present a decent readable interpretation of a Solana
# instruction.
#
class InstructionReporter:
def matches(self, instruction: TransactionInstruction) -> bool:
return True
def report(self, instruction: TransactionInstruction) -> str:
report: typing.List[str] = []
for index, key in enumerate(instruction.keys):
report += [f"Key[{index}]: {key.pubkey} {key.is_signer: <5} {key.is_writable: <5}"]
report += [f"Program ID: {instruction.program_id}"]
report += ["Data: " + "".join("{:02x}".format(x) for x in instruction.data)]
return "\n".join(report)
# # 🥭 SerumInstructionReporter class
#
# The `SerumInstructionParser` class knows a bit more about Serum instructions.
#
class SerumInstructionReporter(InstructionReporter):
def __init__(self, dex_program_id: PublicKey):
self.dex_program_id: PublicKey = dex_program_id
def matches(self, instruction: TransactionInstruction) -> bool:
return instruction.program_id == self.dex_program_id
def report(self, 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) + "»"
# # 🥭 MangoInstructionReporter class
#
# The `MangoInstructionReporter` class knows a bit more about Mango instructions.
#
class MangoInstructionReporter(InstructionReporter):
def __init__(self, program_id: PublicKey):
self.program_id: PublicKey = program_id
def matches(self, instruction: TransactionInstruction) -> bool:
return instruction.program_id == self.program_id
def report(self, 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))
# # 🥭 CompoundInstructionReporter class
#
# The `CompoundInstructionReporter` class can combine multiple `InstructionReporter`s and pick the right one.
#
class CompoundInstructionReporter(InstructionReporter):
def __init__(self, reporters: typing.Sequence[InstructionReporter]):
self.reporters: typing.Sequence[InstructionReporter] = reporters
def matches(self, instruction: TransactionInstruction) -> bool:
for reporter in self.reporters:
if reporter.matches(instruction):
return True
return False
def report(self, instruction: TransactionInstruction) -> str:
for reporter in self.reporters:
if reporter.matches(instruction):
return reporter.report(instruction)
raise Exception(
f"Could not find instruction reporter for instruction {instruction}.")
@staticmethod
def from_ids(program_id: PublicKey, dex_program_id: PublicKey) -> InstructionReporter:
base: InstructionReporter = InstructionReporter()
serum: InstructionReporter = SerumInstructionReporter(dex_program_id)
mango: InstructionReporter = MangoInstructionReporter(program_id)
return CompoundInstructionReporter([mango, serum, base])

296
mango/mangoinstruction.py Normal file
View File

@ -0,0 +1,296 @@
# # ⚠ 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 solana.publickey import PublicKey
from .instructiontype import InstructionType
from .orders import OrderType, Side
# The index of the sender/signer depends on the instruction.
_instruction_signer_indices: typing.Dict[InstructionType, int] = {
InstructionType.InitMangoGroup: 1,
InstructionType.InitMarginAccount: 2,
InstructionType.Deposit: 2,
InstructionType.Withdraw: 2,
InstructionType.AddSpotMarket: 7,
InstructionType.AddToBasket: 2,
InstructionType.Borrow: 2,
InstructionType.CachePrices: -1, # No signer
InstructionType.CacheRootBanks: -1, # No signer
InstructionType.PlaceSpotOrder: 2,
InstructionType.AddOracle: 2,
InstructionType.AddPerpMarket: 6,
InstructionType.PlacePerpOrder: 2,
InstructionType.CancelPerpOrderByClientId: 2,
InstructionType.CancelPerpOrder: 2,
InstructionType.ConsumeEvents: -1, # No signer
InstructionType.CachePerpMarkets: -1, # No signer
InstructionType.UpdateFunding: -1, # No signer
InstructionType.SetOracle: -1, # No signer
InstructionType.SettleFunds: 2,
InstructionType.CancelSpotOrder: 1,
InstructionType.UpdateRootBank: -1, # No signer
InstructionType.SettlePnl: -1, # No signer
InstructionType.SettleBorrow: -1, # No signer
InstructionType.ForceCancelSpotOrders: -1,
InstructionType.ForceCancelPerpOrders: -1,
InstructionType.LiquidateTokenAndToken: -1,
InstructionType.LiquidateTokenAndPerp: -1,
InstructionType.LiquidatePerpMarket: -1,
InstructionType.SettleFees: -1,
InstructionType.ResolvePerpBankruptcy: -1,
InstructionType.ResolveTokenBankruptcy: -1,
InstructionType.InitSpotOpenOrders: -1,
InstructionType.RedeemMngo: -1,
InstructionType.AddMangoAccountInfo: -1,
InstructionType.DepositMsrm: -1,
InstructionType.WithdrawMsrm: -1,
}
# The index of the token IN account depends on the instruction, and for some instructions
# doesn't exist.
_token_in_indices: typing.Dict[InstructionType, int] = {
InstructionType.InitMangoGroup: -1,
InstructionType.InitMarginAccount: -1,
InstructionType.Deposit: 8,
InstructionType.Withdraw: 7,
InstructionType.AddSpotMarket: -1,
InstructionType.AddToBasket: -1,
InstructionType.Borrow: -1,
InstructionType.CachePrices: -1,
InstructionType.CacheRootBanks: -1,
InstructionType.PlaceSpotOrder: -1,
InstructionType.AddOracle: -1,
InstructionType.AddPerpMarket: -1,
InstructionType.PlacePerpOrder: -1,
InstructionType.CancelPerpOrderByClientId: -1,
InstructionType.CancelPerpOrder: -1,
InstructionType.ConsumeEvents: -1,
InstructionType.CachePerpMarkets: -1,
InstructionType.UpdateFunding: -1,
InstructionType.SetOracle: -1,
InstructionType.SettleFunds: -1,
InstructionType.CancelSpotOrder: -1,
InstructionType.UpdateRootBank: -1,
InstructionType.SettlePnl: -1,
InstructionType.SettleBorrow: -1,
InstructionType.ForceCancelSpotOrders: -1,
InstructionType.ForceCancelPerpOrders: -1,
InstructionType.LiquidateTokenAndToken: -1,
InstructionType.LiquidateTokenAndPerp: -1,
InstructionType.LiquidatePerpMarket: -1,
InstructionType.SettleFees: -1,
InstructionType.ResolvePerpBankruptcy: -1,
InstructionType.ResolveTokenBankruptcy: -1,
InstructionType.InitSpotOpenOrders: -1,
InstructionType.RedeemMngo: -1,
InstructionType.AddMangoAccountInfo: -1,
InstructionType.DepositMsrm: -1,
InstructionType.WithdrawMsrm: -1,
}
# The index of the token OUT account depends on the instruction, and for some instructions
# doesn't exist.
_token_out_indices: typing.Dict[InstructionType, int] = {
InstructionType.InitMangoGroup: -1,
InstructionType.InitMarginAccount: -1,
InstructionType.Deposit: 6,
InstructionType.Withdraw: 6,
InstructionType.AddSpotMarket: -1,
InstructionType.AddToBasket: -1,
InstructionType.Borrow: -1,
InstructionType.CachePrices: -1,
InstructionType.CacheRootBanks: -1,
InstructionType.PlaceSpotOrder: -1,
InstructionType.AddOracle: -1,
InstructionType.AddPerpMarket: -1,
InstructionType.PlacePerpOrder: -1,
InstructionType.CancelPerpOrderByClientId: -1,
InstructionType.CancelPerpOrder: -1,
InstructionType.ConsumeEvents: -1,
InstructionType.CachePerpMarkets: -1,
InstructionType.UpdateFunding: -1,
InstructionType.SetOracle: -1,
InstructionType.SettleFunds: -1,
InstructionType.CancelSpotOrder: -1,
InstructionType.UpdateRootBank: -1,
InstructionType.SettlePnl: -1,
InstructionType.SettleBorrow: -1,
InstructionType.ForceCancelSpotOrders: -1,
InstructionType.ForceCancelPerpOrders: -1,
InstructionType.LiquidateTokenAndToken: -1,
InstructionType.LiquidateTokenAndPerp: -1,
InstructionType.LiquidatePerpMarket: -1,
InstructionType.SettleFees: -1,
InstructionType.ResolvePerpBankruptcy: -1,
InstructionType.ResolveTokenBankruptcy: -1,
InstructionType.InitSpotOpenOrders: -1,
InstructionType.RedeemMngo: -1,
InstructionType.AddMangoAccountInfo: -1,
InstructionType.DepositMsrm: -1,
InstructionType.WithdrawMsrm: -1,
}
# Some instructions (like liqudate) have a 'target' account. Most don't.
_target_indices: typing.Dict[InstructionType, int] = {
InstructionType.InitMangoGroup: -1,
InstructionType.InitMarginAccount: -1,
InstructionType.Deposit: -1,
InstructionType.Withdraw: -1,
InstructionType.AddSpotMarket: -1,
InstructionType.AddToBasket: -1,
InstructionType.Borrow: -1,
InstructionType.CachePrices: -1,
InstructionType.CacheRootBanks: -1,
InstructionType.PlaceSpotOrder: -1,
InstructionType.AddOracle: -1,
InstructionType.AddPerpMarket: -1,
InstructionType.PlacePerpOrder: -1,
InstructionType.CancelPerpOrderByClientId: -1,
InstructionType.CancelPerpOrder: -1,
InstructionType.ConsumeEvents: -1,
InstructionType.CachePerpMarkets: -1,
InstructionType.UpdateFunding: -1,
InstructionType.SetOracle: -1,
InstructionType.SettleFunds: -1,
InstructionType.CancelSpotOrder: -1,
InstructionType.UpdateRootBank: -1,
InstructionType.SettlePnl: -1,
InstructionType.SettleBorrow: -1,
InstructionType.ForceCancelSpotOrders: -1,
InstructionType.ForceCancelPerpOrders: -1,
InstructionType.LiquidateTokenAndToken: -1,
InstructionType.LiquidateTokenAndPerp: -1,
InstructionType.LiquidatePerpMarket: -1,
InstructionType.SettleFees: -1,
InstructionType.ResolvePerpBankruptcy: -1,
InstructionType.ResolveTokenBankruptcy: -1,
InstructionType.InitSpotOpenOrders: -1,
InstructionType.RedeemMngo: -1,
InstructionType.AddMangoAccountInfo: -1,
InstructionType.DepositMsrm: -1,
InstructionType.WithdrawMsrm: -1,
}
# # 🥭 MangoInstruction class
#
# 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.
#
class MangoInstruction:
def __init__(self, instruction_type: InstructionType, instruction_data: typing.Any, accounts: typing.Sequence[PublicKey]):
self.instruction_type = instruction_type
self.instruction_data = instruction_data
self.accounts = accounts
@property
def group(self) -> PublicKey:
# Group PublicKey is always the zero index.
return self.accounts[0]
@property
def sender(self) -> typing.Optional[PublicKey]:
account_index = _instruction_signer_indices[self.instruction_type]
if account_index < 0:
return None
return self.accounts[account_index]
@property
def token_in_account(self) -> typing.Optional[PublicKey]:
account_index = _token_in_indices[self.instruction_type]
if account_index < 0:
return None
return self.accounts[account_index]
@property
def token_out_account(self) -> typing.Optional[PublicKey]:
account_index = _token_out_indices[self.instruction_type]
if account_index < 0:
return None
return self.accounts[account_index]
@property
def target_account(self) -> typing.Optional[PublicKey]:
account_index = _target_indices[self.instruction_type]
if account_index < 0:
return None
return self.accounts[account_index]
def describe_parameters(self) -> str:
instruction_type = self.instruction_type
additional_data = ""
if instruction_type == InstructionType.InitMangoGroup:
pass
elif instruction_type == InstructionType.InitMarginAccount:
pass
elif instruction_type == InstructionType.Deposit:
additional_data = f"quantity: {self.instruction_data.quantity}"
elif instruction_type == InstructionType.Withdraw:
additional_data = f"quantity: {self.instruction_data.quantity}, allow_borrow: {self.instruction_data.allow_borrow}"
elif instruction_type == InstructionType.AddSpotMarket:
pass
elif instruction_type == InstructionType.AddToBasket:
pass
elif instruction_type == InstructionType.Borrow:
pass
elif instruction_type == InstructionType.CachePrices:
pass
elif instruction_type == InstructionType.CacheRootBanks:
pass
elif instruction_type == InstructionType.PlaceSpotOrder:
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:
pass
elif instruction_type == InstructionType.AddPerpMarket:
pass
elif instruction_type == InstructionType.PlacePerpOrder:
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:
additional_data = f"client ID: {self.instruction_data.client_order_id}, missing OK: {self.instruction_data.invalid_id_ok}"
elif instruction_type == InstructionType.CancelPerpOrder:
additional_data = f"order ID: {self.instruction_data.order_id}, missing OK: {self.instruction_data.invalid_id_ok}"
elif instruction_type == InstructionType.ConsumeEvents:
additional_data = f"limit: {self.instruction_data.limit}"
elif instruction_type == InstructionType.CachePerpMarkets:
pass
elif instruction_type == InstructionType.UpdateFunding:
pass
elif instruction_type == InstructionType.SetOracle:
pass
elif instruction_type == InstructionType.SettleFunds:
pass
elif instruction_type == InstructionType.CancelSpotOrder:
additional_data = f"order ID: {self.instruction_data.order_id}, side: {Side.from_value(self.instruction_data.side)}"
elif instruction_type == InstructionType.UpdateRootBank:
pass
elif instruction_type == InstructionType.SettlePnl:
pass
elif instruction_type == InstructionType.SettleBorrow:
pass
return additional_data
def __str__(self) -> str:
parameters = self.describe_parameters() or "None"
return f"« {self.instruction_type.name}: {parameters} »"
def __repr__(self) -> str:
return f"{self}"

View File

@ -82,7 +82,7 @@ class MarketMaker:
self.pulse_complete.on_next(datetime.now())
except Exception as exception:
self.logger.error(f"[{context.name}] Market-maker error on pulse: {exception} - {traceback.format_exc()}")
self.logger.error(f"[{context.name}] Market-maker error on pulse:\n{traceback.format_exc()}")
self.pulse_error.on_next(exception)
def __str__(self) -> str:

View File

@ -71,7 +71,8 @@ class SerumOracle(Oracle):
"mainnet-beta",
"https://solana-api.projectserum.com",
context.client.commitment,
context.client.skip_preflight)
context.client.skip_preflight,
context.client.instruction_reporter)
mainnet_serum_market_lookup: SerumMarketLookup = SerumMarketLookup.load(SplTokenLookup.DefaultDataFilepath)
adjusted_market = self.market
mainnet_adjusted_market: typing.Optional[Market] = mainnet_serum_market_lookup.find_by_symbol(

View File

@ -24,9 +24,9 @@ from decimal import Decimal
from solana.publickey import PublicKey
from .context import Context
from .instructionreporter import MangoInstruction
from .instructiontype import InstructionType
from .layouts import layouts
from .orders import OrderType, Side
from .ownedtokenvalue import OwnedTokenValue
from .tokenvalue import TokenValue
@ -62,321 +62,8 @@ from .tokenvalue import TokenValue
#
# The index of the sender/signer depends on the instruction.
_instruction_signer_indices: typing.Dict[InstructionType, int] = {
InstructionType.InitMangoGroup: 1,
InstructionType.InitMarginAccount: 2,
InstructionType.Deposit: 2,
InstructionType.Withdraw: 2,
InstructionType.AddSpotMarket: 7,
InstructionType.AddToBasket: 2,
InstructionType.Borrow: 2,
InstructionType.CachePrices: -1, # No signer
InstructionType.CacheRootBanks: -1, # No signer
InstructionType.PlaceSpotOrder: 2,
InstructionType.AddOracle: 2,
InstructionType.AddPerpMarket: 6,
InstructionType.PlacePerpOrder: 2,
InstructionType.CancelPerpOrderByClientId: 2,
InstructionType.CancelPerpOrder: 2,
InstructionType.ConsumeEvents: -1, # No signer
InstructionType.CachePerpMarkets: -1, # No signer
InstructionType.UpdateFunding: -1, # No signer
InstructionType.SetOracle: -1, # No signer
InstructionType.SettleFunds: 2,
InstructionType.CancelSpotOrder: 1,
InstructionType.UpdateRootBank: -1, # No signer
InstructionType.SettlePnl: -1, # No signer
InstructionType.SettleBorrow: -1, # No signer
InstructionType.ForceCancelSpotOrders: -1,
InstructionType.ForceCancelPerpOrders: -1,
InstructionType.LiquidateTokenAndToken: -1,
InstructionType.LiquidateTokenAndPerp: -1,
InstructionType.LiquidatePerpMarket: -1,
InstructionType.SettleFees: -1,
InstructionType.ResolvePerpBankruptcy: -1,
InstructionType.ResolveTokenBankruptcy: -1,
InstructionType.InitSpotOpenOrders: -1,
InstructionType.RedeemMngo: -1,
InstructionType.AddMangoAccountInfo: -1,
InstructionType.DepositMsrm: -1,
InstructionType.WithdrawMsrm: -1,
}
# The index of the token IN account depends on the instruction, and for some instructions
# doesn't exist.
_token_in_indices: typing.Dict[InstructionType, int] = {
InstructionType.InitMangoGroup: -1,
InstructionType.InitMarginAccount: -1,
InstructionType.Deposit: 8,
InstructionType.Withdraw: 7,
InstructionType.AddSpotMarket: -1,
InstructionType.AddToBasket: -1,
InstructionType.Borrow: -1,
InstructionType.CachePrices: -1,
InstructionType.CacheRootBanks: -1,
InstructionType.PlaceSpotOrder: -1,
InstructionType.AddOracle: -1,
InstructionType.AddPerpMarket: -1,
InstructionType.PlacePerpOrder: -1,
InstructionType.CancelPerpOrderByClientId: -1,
InstructionType.CancelPerpOrder: -1,
InstructionType.ConsumeEvents: -1,
InstructionType.CachePerpMarkets: -1,
InstructionType.UpdateFunding: -1,
InstructionType.SetOracle: -1,
InstructionType.SettleFunds: -1,
InstructionType.CancelSpotOrder: -1,
InstructionType.UpdateRootBank: -1,
InstructionType.SettlePnl: -1,
InstructionType.SettleBorrow: -1,
InstructionType.ForceCancelSpotOrders: -1,
InstructionType.ForceCancelPerpOrders: -1,
InstructionType.LiquidateTokenAndToken: -1,
InstructionType.LiquidateTokenAndPerp: -1,
InstructionType.LiquidatePerpMarket: -1,
InstructionType.SettleFees: -1,
InstructionType.ResolvePerpBankruptcy: -1,
InstructionType.ResolveTokenBankruptcy: -1,
InstructionType.InitSpotOpenOrders: -1,
InstructionType.RedeemMngo: -1,
InstructionType.AddMangoAccountInfo: -1,
InstructionType.DepositMsrm: -1,
InstructionType.WithdrawMsrm: -1,
}
# The index of the token OUT account depends on the instruction, and for some instructions
# doesn't exist.
_token_out_indices: typing.Dict[InstructionType, int] = {
InstructionType.InitMangoGroup: -1,
InstructionType.InitMarginAccount: -1,
InstructionType.Deposit: 6,
InstructionType.Withdraw: 6,
InstructionType.AddSpotMarket: -1,
InstructionType.AddToBasket: -1,
InstructionType.Borrow: -1,
InstructionType.CachePrices: -1,
InstructionType.CacheRootBanks: -1,
InstructionType.PlaceSpotOrder: -1,
InstructionType.AddOracle: -1,
InstructionType.AddPerpMarket: -1,
InstructionType.PlacePerpOrder: -1,
InstructionType.CancelPerpOrderByClientId: -1,
InstructionType.CancelPerpOrder: -1,
InstructionType.ConsumeEvents: -1,
InstructionType.CachePerpMarkets: -1,
InstructionType.UpdateFunding: -1,
InstructionType.SetOracle: -1,
InstructionType.SettleFunds: -1,
InstructionType.CancelSpotOrder: -1,
InstructionType.UpdateRootBank: -1,
InstructionType.SettlePnl: -1,
InstructionType.SettleBorrow: -1,
InstructionType.ForceCancelSpotOrders: -1,
InstructionType.ForceCancelPerpOrders: -1,
InstructionType.LiquidateTokenAndToken: -1,
InstructionType.LiquidateTokenAndPerp: -1,
InstructionType.LiquidatePerpMarket: -1,
InstructionType.SettleFees: -1,
InstructionType.ResolvePerpBankruptcy: -1,
InstructionType.ResolveTokenBankruptcy: -1,
InstructionType.InitSpotOpenOrders: -1,
InstructionType.RedeemMngo: -1,
InstructionType.AddMangoAccountInfo: -1,
InstructionType.DepositMsrm: -1,
InstructionType.WithdrawMsrm: -1,
}
# Some instructions (like liqudate) have a 'target' account. Most don't.
_target_indices: typing.Dict[InstructionType, int] = {
InstructionType.InitMangoGroup: -1,
InstructionType.InitMarginAccount: -1,
InstructionType.Deposit: -1,
InstructionType.Withdraw: -1,
InstructionType.AddSpotMarket: -1,
InstructionType.AddToBasket: -1,
InstructionType.Borrow: -1,
InstructionType.CachePrices: -1,
InstructionType.CacheRootBanks: -1,
InstructionType.PlaceSpotOrder: -1,
InstructionType.AddOracle: -1,
InstructionType.AddPerpMarket: -1,
InstructionType.PlacePerpOrder: -1,
InstructionType.CancelPerpOrderByClientId: -1,
InstructionType.CancelPerpOrder: -1,
InstructionType.ConsumeEvents: -1,
InstructionType.CachePerpMarkets: -1,
InstructionType.UpdateFunding: -1,
InstructionType.SetOracle: -1,
InstructionType.SettleFunds: -1,
InstructionType.CancelSpotOrder: -1,
InstructionType.UpdateRootBank: -1,
InstructionType.SettlePnl: -1,
InstructionType.SettleBorrow: -1,
InstructionType.ForceCancelSpotOrders: -1,
InstructionType.ForceCancelPerpOrders: -1,
InstructionType.LiquidateTokenAndToken: -1,
InstructionType.LiquidateTokenAndPerp: -1,
InstructionType.LiquidatePerpMarket: -1,
InstructionType.SettleFees: -1,
InstructionType.ResolvePerpBankruptcy: -1,
InstructionType.ResolveTokenBankruptcy: -1,
InstructionType.InitSpotOpenOrders: -1,
InstructionType.RedeemMngo: -1,
InstructionType.AddMangoAccountInfo: -1,
InstructionType.DepositMsrm: -1,
InstructionType.WithdrawMsrm: -1,
}
# # 🥭 MangoInstruction class
#
# 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.
#
class MangoInstruction:
def __init__(self, instruction_type: InstructionType, instruction_data: typing.Any, accounts: typing.Sequence[PublicKey]):
self.instruction_type = instruction_type
self.instruction_data = instruction_data
self.accounts = accounts
@property
def group(self) -> PublicKey:
# Group PublicKey is always the zero index.
return self.accounts[0]
@property
def sender(self) -> typing.Optional[PublicKey]:
account_index = _instruction_signer_indices[self.instruction_type]
if account_index < 0:
return None
return self.accounts[account_index]
@property
def token_in_account(self) -> typing.Optional[PublicKey]:
account_index = _token_in_indices[self.instruction_type]
if account_index < 0:
return None
return self.accounts[account_index]
@property
def token_out_account(self) -> typing.Optional[PublicKey]:
account_index = _token_out_indices[self.instruction_type]
if account_index < 0:
return None
return self.accounts[account_index]
@property
def target_account(self) -> typing.Optional[PublicKey]:
account_index = _target_indices[self.instruction_type]
if account_index < 0:
return None
return self.accounts[account_index]
def describe_parameters(self) -> str:
instruction_type = self.instruction_type
additional_data = ""
if instruction_type == InstructionType.InitMangoGroup:
pass
elif instruction_type == InstructionType.InitMarginAccount:
pass
elif instruction_type == InstructionType.Deposit:
additional_data = f"quantity: {self.instruction_data.quantity}"
elif instruction_type == InstructionType.Withdraw:
additional_data = f"quantity: {self.instruction_data.quantity}, allow_borrow: {self.instruction_data.allow_borrow}"
elif instruction_type == InstructionType.AddSpotMarket:
pass
elif instruction_type == InstructionType.AddToBasket:
pass
elif instruction_type == InstructionType.Borrow:
pass
elif instruction_type == InstructionType.CachePrices:
pass
elif instruction_type == InstructionType.CacheRootBanks:
pass
elif instruction_type == InstructionType.PlaceSpotOrder:
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:
pass
elif instruction_type == InstructionType.AddPerpMarket:
pass
elif instruction_type == InstructionType.PlacePerpOrder:
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:
additional_data = f"client ID: {self.instruction_data.client_order_id}, missing OK: {self.instruction_data.invalid_id_ok}"
elif instruction_type == InstructionType.CancelPerpOrder:
additional_data = f"order ID: {self.instruction_data.order_id}, missing OK: {self.instruction_data.invalid_id_ok}"
elif instruction_type == InstructionType.ConsumeEvents:
additional_data = f"limit: {self.instruction_data.limit}"
elif instruction_type == InstructionType.CachePerpMarkets:
pass
elif instruction_type == InstructionType.UpdateFunding:
pass
elif instruction_type == InstructionType.SetOracle:
pass
elif instruction_type == InstructionType.SettleFunds:
pass
elif instruction_type == InstructionType.CancelSpotOrder:
additional_data = f"order ID: {self.instruction_data.order_id}, side: {Side.from_value(self.instruction_data.side)}"
elif instruction_type == InstructionType.UpdateRootBank:
pass
elif instruction_type == InstructionType.SettlePnl:
pass
elif instruction_type == InstructionType.SettleBorrow:
pass
return additional_data
@staticmethod
def from_response(context: Context, all_accounts: typing.Sequence[PublicKey], instruction_data: typing.Dict) -> typing.Optional["MangoInstruction"]:
program_account_index = instruction_data["programIdIndex"]
if all_accounts[program_account_index] != context.program_id:
# It's an instruction, it's just not a Mango one.
return None
data = instruction_data["data"]
instructions_account_indices = instruction_data["accounts"]
decoded = base58.b58decode(data)
initial = layouts.MANGO_INSTRUCTION_VARIANT_FINDER.parse(decoded)
parser = layouts.InstructionParsersByVariant[initial.variant]
if parser is None:
logging.warning(
f"Could not find instruction parser for variant {initial.variant} / {InstructionType(initial.variant)}.")
return None
# A whole bunch of accounts are listed for a transaction. Some (or all) of them apply
# to this instruction. The instruction data gives the index of each account it uses,
# in the order in which it uses them. So, for example, if it uses 3 accounts, the
# instruction data could say [3, 2, 14], meaning the first account it uses is index 3
# in the whole transaction account list, the second is index 2 in the whole transaction
# account list, the third is index 14 in the whole transaction account list.
accounts: typing.List[PublicKey] = []
for index in instructions_account_indices:
accounts += [all_accounts[index]]
parsed = parser.parse(decoded)
instruction_type = InstructionType(int(parsed.variant))
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:
parameters = self.describe_parameters() or "None"
return f"« {self.instruction_type.name}: {parameters} »"
def __repr__(self) -> str:
return f"{self}"
# # 🥭 TransactionScout class
#
class TransactionScout:
def __init__(self, timestamp: datetime.datetime, signatures: typing.Sequence[str],
succeeded: bool, group_name: str, accounts: typing.Sequence[PublicKey],
@ -457,7 +144,7 @@ class TransactionScout:
accounts = list(map(PublicKey, response["transaction"]["message"]["accountKeys"]))
instructions: typing.List[MangoInstruction] = []
for instruction_data in response["transaction"]["message"]["instructions"]:
instruction = MangoInstruction.from_response(context, accounts, instruction_data)
instruction = mango_instruction_from_response(context, accounts, instruction_data)
if instruction is not None:
instructions += [instruction]
@ -541,3 +228,36 @@ def fetch_all_recent_transaction_signatures(context: Context) -> typing.Sequence
before = signature_results[-1]
return signature_results
def mango_instruction_from_response(context: Context, all_accounts: typing.Sequence[PublicKey], instruction_data: typing.Dict) -> typing.Optional["MangoInstruction"]:
program_account_index = instruction_data["programIdIndex"]
if all_accounts[program_account_index] != context.program_id:
# It's an instruction, it's just not a Mango one.
return None
data = instruction_data["data"]
instructions_account_indices = instruction_data["accounts"]
decoded = base58.b58decode(data)
initial = layouts.MANGO_INSTRUCTION_VARIANT_FINDER.parse(decoded)
parser = layouts.InstructionParsersByVariant[initial.variant]
if parser is None:
logging.warning(
f"Could not find instruction parser for variant {initial.variant} / {InstructionType(initial.variant)}.")
return None
# A whole bunch of accounts are listed for a transaction. Some (or all) of them apply
# to this instruction. The instruction data gives the index of each account it uses,
# in the order in which it uses them. So, for example, if it uses 3 accounts, the
# instruction data could say [3, 2, 14], meaning the first account it uses is index 3
# in the whole transaction account list, the second is index 2 in the whole transaction
# account list, the third is index 14 in the whole transaction account list.
accounts: typing.List[PublicKey] = []
for index in instructions_account_indices:
accounts += [all_accounts[index]]
parsed = parser.parse(decoded)
instruction_type = InstructionType(int(parsed.variant))
return MangoInstruction(instruction_type, parsed, accounts)

View File

@ -12,7 +12,7 @@ from typing import NamedTuple
class MockCompatibleClient(mango.CompatibleClient):
def __init__(self):
super().__init__("test", "local", "http://localhost", "processed", "base64")
super().__init__("test", "local", "http://localhost", "processed", "base64", mango.InstructionReporter())
self.token_accounts_by_owner = []
def get_token_accounts_by_owner(self, *args, **kwargs) -> RPCResponse: