Improved transaction exception reporting.
This commit is contained in:
parent
a51b38049b
commit
a96b67e729
|
@ -22,11 +22,13 @@ from .idsjsontokenlookup import IdsJsonTokenLookup
|
||||||
from .idsjsonmarketlookup import IdsJsonMarketLookup
|
from .idsjsonmarketlookup import IdsJsonMarketLookup
|
||||||
from .inventory import Inventory, InventoryAccountWatcher, spl_token_inventory_loader, account_inventory_loader
|
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 .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 .instructiontype import InstructionType
|
||||||
from .liquidatablereport import LiquidatableState, LiquidatableReport
|
from .liquidatablereport import LiquidatableState, LiquidatableReport
|
||||||
from .liquidationevent import LiquidationEvent
|
from .liquidationevent import LiquidationEvent
|
||||||
from .liquidationprocessor import LiquidationProcessor, LiquidationProcessorState
|
from .liquidationprocessor import LiquidationProcessor, LiquidationProcessorState
|
||||||
from .lotsizeconverter import LotSizeConverter, NullLotSizeConverter
|
from .lotsizeconverter import LotSizeConverter, NullLotSizeConverter
|
||||||
|
from .mangoinstruction import MangoInstruction
|
||||||
from .market import InventorySource, Market
|
from .market import InventorySource, Market
|
||||||
from .marketinstructionbuilder import MarketInstructionBuilder, NullMarketInstructionBuilder
|
from .marketinstructionbuilder import MarketInstructionBuilder, NullMarketInstructionBuilder
|
||||||
from .marketlookup import MarketLookup, NullMarketLookup, CompoundMarketLookup
|
from .marketlookup import MarketLookup, NullMarketLookup, CompoundMarketLookup
|
||||||
|
@ -69,7 +71,7 @@ from .tokeninfo import TokenInfo
|
||||||
from .tokenlookup import TokenLookup, NullTokenLookup, CompoundTokenLookup
|
from .tokenlookup import TokenLookup, NullTokenLookup, CompoundTokenLookup
|
||||||
from .tokenvalue import TokenValue
|
from .tokenvalue import TokenValue
|
||||||
from .tradeexecutor import TradeExecutor, NullTradeExecutor, ImmediateTradeExecutor
|
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 .version import Version
|
||||||
from .wallet import Wallet
|
from .wallet import Wallet
|
||||||
from .walletbalancer import TargetBalance, FixedTargetBalance, PercentageTargetBalance, TargetBalanceParser, sort_changes_for_trades, calculate_required_balance_changes, FilterSmallChanges, WalletBalancer, NullWalletBalancer, LiveWalletBalancer
|
from .walletbalancer import TargetBalance, FixedTargetBalance, PercentageTargetBalance, TargetBalanceParser, sort_changes_for_trades, calculate_required_balance_changes, FilterSmallChanges, WalletBalancer, NullWalletBalancer, LiveWalletBalancer
|
||||||
|
|
|
@ -32,6 +32,7 @@ from solana.rpc.commitment import Commitment
|
||||||
from solana.rpc.types import DataSliceOpts, MemcmpOpts, RPCResponse, TokenAccountOpts, TxOpts
|
from solana.rpc.types import DataSliceOpts, MemcmpOpts, RPCResponse, TokenAccountOpts, TxOpts
|
||||||
|
|
||||||
from .constants import SOL_DECIMAL_DIVISOR
|
from .constants import SOL_DECIMAL_DIVISOR
|
||||||
|
from .instructionreporter import InstructionReporter
|
||||||
|
|
||||||
|
|
||||||
# # 🥭 RateLimitException class
|
# # 🥭 RateLimitException class
|
||||||
|
@ -67,8 +68,9 @@ class TooManyRequestsRateLimitException(RateLimitException):
|
||||||
# of problems at the right place.
|
# of problems at the right place.
|
||||||
#
|
#
|
||||||
class TransactionException(Exception):
|
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)
|
super().__init__(message)
|
||||||
|
self.transaction: typing.Optional[Transaction] = transaction
|
||||||
self.message: str = message
|
self.message: str = message
|
||||||
self.code: int = code
|
self.code: int = code
|
||||||
self.name: str = name
|
self.name: str = name
|
||||||
|
@ -84,8 +86,13 @@ class TransactionException(Exception):
|
||||||
self.accounts: typing.List[str] = _ensure_list(accounts)
|
self.accounts: typing.List[str] = _ensure_list(accounts)
|
||||||
self.errors: typing.List[str] = _ensure_list(errors)
|
self.errors: typing.List[str] = _ensure_list(errors)
|
||||||
self.logs: typing.List[str] = _ensure_list(logs)
|
self.logs: typing.List[str] = _ensure_list(logs)
|
||||||
|
self.instruction_reporter: InstructionReporter = instruction_reporter
|
||||||
|
|
||||||
def __str__(self) -> str:
|
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"
|
accounts = "No Accounts"
|
||||||
if len(self.accounts) > 0:
|
if len(self.accounts) > 0:
|
||||||
accounts = "\n ".join([f"{item}".replace("\n", "\n ") for item in self.accounts])
|
accounts = "\n ".join([f"{item}".replace("\n", "\n ") for item in self.accounts])
|
||||||
|
@ -95,7 +102,7 @@ class TransactionException(Exception):
|
||||||
logs = "No Logs"
|
logs = "No Logs"
|
||||||
if len(self.logs) > 0:
|
if len(self.logs) > 0:
|
||||||
logs = "\n ".join([f"{item}".replace("\n", "\n ") for item in self.logs])
|
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:
|
||||||
{accounts}
|
{accounts}
|
||||||
Errors:
|
Errors:
|
||||||
|
@ -127,16 +134,16 @@ UnspecifiedEncoding = "unspecified"
|
||||||
# some common operations better from our point of view.
|
# some common operations better from our point of view.
|
||||||
#
|
#
|
||||||
class CompatibleClient:
|
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.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
||||||
self.name: str = name
|
self.name: str = name
|
||||||
self.cluster: str = cluster
|
self.cluster: str = cluster
|
||||||
self.cluster_url: str = cluster_url
|
self.cluster_url: str = cluster_url
|
||||||
|
|
||||||
self._request_counter = itertools.count()
|
|
||||||
|
|
||||||
self.commitment: Commitment = commitment
|
self.commitment: Commitment = commitment
|
||||||
self.skip_preflight: bool = skip_preflight
|
self.skip_preflight: bool = skip_preflight
|
||||||
|
self.instruction_reporter: InstructionReporter = instruction_reporter
|
||||||
|
|
||||||
|
self._request_counter = itertools.count()
|
||||||
self.encoding: str = "base64"
|
self.encoding: str = "base64"
|
||||||
|
|
||||||
def is_node_healthy(self) -> bool:
|
def is_node_healthy(self) -> bool:
|
||||||
|
@ -250,6 +257,7 @@ class CompatibleClient:
|
||||||
|
|
||||||
skip_preflight: bool = opts.skip_preflight or self.skip_preflight
|
skip_preflight: bool = opts.skip_preflight or self.skip_preflight
|
||||||
|
|
||||||
|
try:
|
||||||
return self._send_request(
|
return self._send_request(
|
||||||
"sendTransaction",
|
"sendTransaction",
|
||||||
encoded_transaction,
|
encoded_transaction,
|
||||||
|
@ -259,6 +267,11 @@ class CompatibleClient:
|
||||||
_EncodingKey: self.encoding,
|
_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:
|
def _send_request(self, method: str, *params: typing.Any) -> RPCResponse:
|
||||||
request_id = next(self._request_counter) + 1
|
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_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_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"
|
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)
|
error_accounts, error_err, error_logs)
|
||||||
|
|
||||||
# The call succeeded.
|
# The call succeeded.
|
||||||
|
@ -377,9 +390,17 @@ class BetterClient:
|
||||||
def skip_preflight(self, value: bool) -> None:
|
def skip_preflight(self, value: bool) -> None:
|
||||||
self.compatible_client.skip_preflight = value
|
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
|
@staticmethod
|
||||||
def from_configuration(name: str, cluster: str, cluster_url: str, commitment: Commitment, skip_preflight: bool) -> "BetterClient":
|
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)
|
compatible = CompatibleClient(name, cluster, cluster_url, commitment, skip_preflight, instruction_reporter)
|
||||||
return BetterClient(compatible)
|
return BetterClient(compatible)
|
||||||
|
|
||||||
def is_node_healthy(self) -> bool:
|
def is_node_healthy(self) -> bool:
|
||||||
|
|
|
@ -16,58 +16,19 @@
|
||||||
import logging
|
import logging
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from pyserum._layouts.instructions import InstructionType as SerumInstructionType
|
|
||||||
from solana.account import Account as SolanaAccount
|
from solana.account import Account as SolanaAccount
|
||||||
from solana.blockhash import Blockhash
|
from solana.blockhash import Blockhash
|
||||||
from solana.publickey import PublicKey
|
from solana.publickey import PublicKey
|
||||||
from solana.transaction import Transaction, TransactionInstruction
|
from solana.transaction import Transaction, TransactionInstruction
|
||||||
|
|
||||||
from .context import Context
|
from .context import Context
|
||||||
from .layouts import layouts
|
from .instructionreporter import InstructionReporter
|
||||||
from .transactionscout import MangoInstruction, InstructionType
|
|
||||||
from .wallet import Wallet
|
from .wallet import Wallet
|
||||||
|
|
||||||
_MAXIMUM_TRANSACTION_LENGTH = 1280 - 40 - 8
|
_MAXIMUM_TRANSACTION_LENGTH = 1280 - 40 - 8
|
||||||
_SIGNATURE_LENGTH = 64
|
_SIGNATURE_LENGTH = 64
|
||||||
|
|
||||||
|
|
||||||
def _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]]:
|
def _split_instructions_into_chunks(signers: typing.Sequence[SolanaAccount], instructions: typing.Sequence[TransactionInstruction]) -> typing.Sequence[typing.Sequence[TransactionInstruction]]:
|
||||||
vetted_chunks: typing.List[typing.List[TransactionInstruction]] = []
|
vetted_chunks: typing.List[typing.List[TransactionInstruction]] = []
|
||||||
current_chunk: typing.List[TransactionInstruction] = []
|
current_chunk: typing.List[TransactionInstruction] = []
|
||||||
|
@ -173,12 +134,10 @@ class CombinableInstructions():
|
||||||
results += [response]
|
results += [response]
|
||||||
except Exception as exception:
|
except Exception as exception:
|
||||||
starts_at = sum(len(ch) for ch in chunks[0:index])
|
starts_at = sum(len(ch) for ch in chunks[0:index])
|
||||||
instruction_text = "\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.
|
self.logger.error(f"""[{context.name}] Error executing chunk {index} (instructions {starts_at} to {starts_at + len(chunk)}) of CombinableInstruction.
|
||||||
Exception: {exception}
|
{exception}""")
|
||||||
Failing instruction(s):
|
else:
|
||||||
{instruction_text}""")
|
|
||||||
if not on_exception_continue:
|
|
||||||
raise exception
|
raise exception
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
@ -188,8 +147,9 @@ Failing instruction(s):
|
||||||
for index, signer in enumerate(self.signers):
|
for index, signer in enumerate(self.signers):
|
||||||
report += [f"Signer[{index}]: {signer.public_key()}"]
|
report += [f"Signer[{index}]: {signer.public_key()}"]
|
||||||
|
|
||||||
|
instruction_reporter: InstructionReporter = InstructionReporter()
|
||||||
for instruction in self.instructions:
|
for instruction in self.instructions:
|
||||||
report += [_raw_instruction_to_str(instruction)]
|
report += [instruction_reporter.report(instruction)]
|
||||||
|
|
||||||
return "\n".join(report)
|
return "\n".join(report)
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ from solana.rpc.commitment import Commitment
|
||||||
|
|
||||||
from .client import BetterClient
|
from .client import BetterClient
|
||||||
from .constants import MangoConstants
|
from .constants import MangoConstants
|
||||||
|
from .instructionreporter import InstructionReporter, CompoundInstructionReporter
|
||||||
from .marketlookup import MarketLookup
|
from .marketlookup import MarketLookup
|
||||||
from .tokenlookup import TokenLookup
|
from .tokenlookup import TokenLookup
|
||||||
|
|
||||||
|
@ -45,8 +46,9 @@ class Context:
|
||||||
group_name: str, group_id: PublicKey, token_lookup: TokenLookup, market_lookup: MarketLookup):
|
group_name: str, group_id: PublicKey, token_lookup: TokenLookup, market_lookup: MarketLookup):
|
||||||
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
||||||
self.name: str = name
|
self.name: str = name
|
||||||
|
instruction_reporter: InstructionReporter = CompoundInstructionReporter.from_ids(program_id, dex_program_id)
|
||||||
self.client: BetterClient = BetterClient.from_configuration(
|
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.program_id: PublicKey = program_id
|
||||||
self.dex_program_id: PublicKey = dex_program_id
|
self.dex_program_id: PublicKey = dex_program_id
|
||||||
self.group_name: str = group_name
|
self.group_name: str = group_name
|
||||||
|
|
|
@ -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])
|
|
@ -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}"
|
|
@ -82,7 +82,7 @@ class MarketMaker:
|
||||||
|
|
||||||
self.pulse_complete.on_next(datetime.now())
|
self.pulse_complete.on_next(datetime.now())
|
||||||
except Exception as exception:
|
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)
|
self.pulse_error.on_next(exception)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
|
|
|
@ -71,7 +71,8 @@ class SerumOracle(Oracle):
|
||||||
"mainnet-beta",
|
"mainnet-beta",
|
||||||
"https://solana-api.projectserum.com",
|
"https://solana-api.projectserum.com",
|
||||||
context.client.commitment,
|
context.client.commitment,
|
||||||
context.client.skip_preflight)
|
context.client.skip_preflight,
|
||||||
|
context.client.instruction_reporter)
|
||||||
mainnet_serum_market_lookup: SerumMarketLookup = SerumMarketLookup.load(SplTokenLookup.DefaultDataFilepath)
|
mainnet_serum_market_lookup: SerumMarketLookup = SerumMarketLookup.load(SplTokenLookup.DefaultDataFilepath)
|
||||||
adjusted_market = self.market
|
adjusted_market = self.market
|
||||||
mainnet_adjusted_market: typing.Optional[Market] = mainnet_serum_market_lookup.find_by_symbol(
|
mainnet_adjusted_market: typing.Optional[Market] = mainnet_serum_market_lookup.find_by_symbol(
|
||||||
|
|
|
@ -24,9 +24,9 @@ from decimal import Decimal
|
||||||
from solana.publickey import PublicKey
|
from solana.publickey import PublicKey
|
||||||
|
|
||||||
from .context import Context
|
from .context import Context
|
||||||
|
from .instructionreporter import MangoInstruction
|
||||||
from .instructiontype import InstructionType
|
from .instructiontype import InstructionType
|
||||||
from .layouts import layouts
|
from .layouts import layouts
|
||||||
from .orders import OrderType, Side
|
|
||||||
from .ownedtokenvalue import OwnedTokenValue
|
from .ownedtokenvalue import OwnedTokenValue
|
||||||
from .tokenvalue import TokenValue
|
from .tokenvalue import TokenValue
|
||||||
|
|
||||||
|
@ -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
|
# # 🥭 TransactionScout class
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
class TransactionScout:
|
class TransactionScout:
|
||||||
def __init__(self, timestamp: datetime.datetime, signatures: typing.Sequence[str],
|
def __init__(self, timestamp: datetime.datetime, signatures: typing.Sequence[str],
|
||||||
succeeded: bool, group_name: str, accounts: typing.Sequence[PublicKey],
|
succeeded: bool, group_name: str, accounts: typing.Sequence[PublicKey],
|
||||||
|
@ -457,7 +144,7 @@ class TransactionScout:
|
||||||
accounts = list(map(PublicKey, response["transaction"]["message"]["accountKeys"]))
|
accounts = list(map(PublicKey, response["transaction"]["message"]["accountKeys"]))
|
||||||
instructions: typing.List[MangoInstruction] = []
|
instructions: typing.List[MangoInstruction] = []
|
||||||
for instruction_data in response["transaction"]["message"]["instructions"]:
|
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:
|
if instruction is not None:
|
||||||
instructions += [instruction]
|
instructions += [instruction]
|
||||||
|
|
||||||
|
@ -541,3 +228,36 @@ def fetch_all_recent_transaction_signatures(context: Context) -> typing.Sequence
|
||||||
before = signature_results[-1]
|
before = signature_results[-1]
|
||||||
|
|
||||||
return signature_results
|
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)
|
||||||
|
|
|
@ -12,7 +12,7 @@ from typing import NamedTuple
|
||||||
|
|
||||||
class MockCompatibleClient(mango.CompatibleClient):
|
class MockCompatibleClient(mango.CompatibleClient):
|
||||||
def __init__(self):
|
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 = []
|
self.token_accounts_by_owner = []
|
||||||
|
|
||||||
def get_token_accounts_by_owner(self, *args, **kwargs) -> RPCResponse:
|
def get_token_accounts_by_owner(self, *args, **kwargs) -> RPCResponse:
|
||||||
|
|
Loading…
Reference in New Issue