diff --git a/mango/__init__.py b/mango/__init__.py index 5eca437..96e823c 100644 --- a/mango/__init__.py +++ b/mango/__init__.py @@ -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 diff --git a/mango/client.py b/mango/client.py index 528a690..57f5fe5 100644 --- a/mango/client.py +++ b/mango/client.py @@ -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,15 +257,21 @@ class CompatibleClient: skip_preflight: bool = opts.skip_preflight or self.skip_preflight - return self._send_request( - "sendTransaction", - encoded_transaction, - { - _SkipPreflightKey: skip_preflight, - _PreflightCommitmentKey: commitment, - _EncodingKey: self.encoding, - } - ) + try: + return self._send_request( + "sendTransaction", + encoded_transaction, + { + _SkipPreflightKey: skip_preflight, + _PreflightCommitmentKey: commitment, + _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: diff --git a/mango/combinableinstructions.py b/mango/combinableinstructions.py index a9866af..96be4db 100644 --- a/mango/combinableinstructions.py +++ b/mango/combinableinstructions.py @@ -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))) - 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: + 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}""") + 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) diff --git a/mango/context.py b/mango/context.py index b8e29b5..75b1e30 100644 --- a/mango/context.py +++ b/mango/context.py @@ -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 diff --git a/mango/instructionreporter.py b/mango/instructionreporter.py new file mode 100644 index 0000000..7dc21df --- /dev/null +++ b/mango/instructionreporter.py @@ -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]) diff --git a/mango/mangoinstruction.py b/mango/mangoinstruction.py new file mode 100644 index 0000000..9b3150f --- /dev/null +++ b/mango/mangoinstruction.py @@ -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}" diff --git a/mango/marketmaking/marketmaker.py b/mango/marketmaking/marketmaker.py index 086e2e8..7afd108 100644 --- a/mango/marketmaking/marketmaker.py +++ b/mango/marketmaking/marketmaker.py @@ -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: diff --git a/mango/oracles/serum/serum.py b/mango/oracles/serum/serum.py index f871a8b..6432ac9 100644 --- a/mango/oracles/serum/serum.py +++ b/mango/oracles/serum/serum.py @@ -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( diff --git a/mango/transactionscout.py b/mango/transactionscout.py index 7f80680..5da3aaa 100644 --- a/mango/transactionscout.py +++ b/mango/transactionscout.py @@ -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) diff --git a/tests/fakes.py b/tests/fakes.py index d74ab10..7ecf88a 100644 --- a/tests/fakes.py +++ b/tests/fakes.py @@ -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: