mango-explorer/mango/transactionscout.py

344 lines
13 KiB
Python

# # ⚠ 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 base58
import logging
import traceback
import typing
from datetime import datetime
from decimal import Decimal
from solana.publickey import PublicKey
from .context import Context
from .datetimes import datetime_from_timestamp
from .instructiontype import InstructionType
from .instrumentvalue import InstrumentValue
from .logmessages import expand_log_messages
from .mangoinstruction import MangoInstruction
from .layouts import layouts
from .ownedinstrumentvalue import OwnedInstrumentValue
from .text import indent_collection_as_str, indent_item_by
# # 🥭 TransactionScout
#
# ## Transaction Indices
#
# Transactions come with a large account list.
#
# Instructions, individually, take accounts.
#
# The accounts instructions take are listed in the the transaction's list of accounts.
#
# The instruction data therefore doesn't need to specify account public keys, only the
# index of those public keys in the main transaction's list.
#
# So, for example, if an instruction 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.
#
# This complicates figuring out which account is which for a given instruction, especially
# since some of the accounts (like the sender/signer account) can appear at different
# indices depending on which instruction is being used.
#
# We keep a few static dictionaries here to allow us to dereference important accounts per
# type.
#
# In addition, we dereference the accounts for each instruction when we instantiate each
# `MangoInstruction`, so users of `MangoInstruction` don't need to worry about
# these details.
#
# # 🥭 TransactionScout class
#
class TransactionScout:
def __init__(
self,
timestamp: datetime,
signatures: typing.Sequence[str],
succeeded: bool,
group_name: str,
accounts: typing.Sequence[PublicKey],
instructions: typing.Sequence[MangoInstruction],
messages: typing.Sequence[str],
pre_token_balances: typing.Sequence[OwnedInstrumentValue],
post_token_balances: typing.Sequence[OwnedInstrumentValue],
) -> None:
self.timestamp: datetime = timestamp
self.signatures: typing.Sequence[str] = signatures
self.succeeded: bool = succeeded
self.group_name: str = group_name
self.accounts: typing.Sequence[PublicKey] = accounts
self.instructions: typing.Sequence[MangoInstruction] = instructions
self.messages: typing.Sequence[str] = messages
self.pre_token_balances: typing.Sequence[
OwnedInstrumentValue
] = pre_token_balances
self.post_token_balances: typing.Sequence[
OwnedInstrumentValue
] = post_token_balances
@property
def summary(self) -> str:
result = "[Success]" if self.succeeded else "[Failed]"
instructions = ", ".join(
[ins.instruction_type.name for ins in self.instructions]
)
changes = OwnedInstrumentValue.changes(
self.pre_token_balances, self.post_token_balances
)
in_tokens = []
for ins in self.instructions:
if ins.token_in_account is not None:
in_tokens += [
OwnedInstrumentValue.find_by_owner(changes, ins.token_in_account)
]
out_tokens = []
for ins in self.instructions:
if ins.token_out_account is not None:
out_tokens += [
OwnedInstrumentValue.find_by_owner(changes, ins.token_out_account)
]
changed_tokens = in_tokens + out_tokens
changed_tokens_text = (
", ".join(
[
f"{tok.token_value.value:,.8f} {tok.token_value.token.name}"
for tok in changed_tokens
]
)
or "None"
)
return f"« TransactionScout {result} {self.group_name} [{self.timestamp}] {instructions}: Token Changes: {changed_tokens_text}\n {self.signatures} »"
@property
def sender(self) -> typing.Optional[PublicKey]:
return self.instructions[0].sender if len(self.instructions) > 0 else None
@property
def group(self) -> typing.Optional[PublicKey]:
return self.instructions[0].group if len(self.instructions) > 0 else None
def has_any_instruction_of_type(self, instruction_type: InstructionType) -> bool:
return any(
map(lambda ins: ins.instruction_type == instruction_type, self.instructions)
)
@staticmethod
def load_if_available(
context: Context, signature: str
) -> typing.Optional["TransactionScout"]:
transaction_details = context.client.get_confirmed_transaction(signature)
if transaction_details is None:
return None
return TransactionScout.from_transaction_response(context, transaction_details)
@staticmethod
def load(context: Context, signature: str) -> "TransactionScout":
tx = TransactionScout.load_if_available(context, signature)
if tx is None:
raise Exception(f"Transaction '{signature}' not found.")
return tx
@staticmethod
def from_transaction_response(
context: Context, response: typing.Dict[str, typing.Any]
) -> "TransactionScout":
def balance_to_token_value(
accounts: typing.Sequence[PublicKey], balance: typing.Dict[str, typing.Any]
) -> OwnedInstrumentValue:
mint = PublicKey(balance["mint"])
account = accounts[balance["accountIndex"]]
amount = Decimal(balance["uiTokenAmount"]["amount"])
decimals = Decimal(balance["uiTokenAmount"]["decimals"])
divisor = Decimal(10) ** decimals
value = amount / divisor
token = context.instrument_lookup.find_by_mint_or_raise(mint)
return OwnedInstrumentValue(account, InstrumentValue(token, value))
try:
succeeded = True if response["meta"]["err"] is None else False
accounts = list(
map(PublicKey, response["transaction"]["message"]["accountKeys"])
)
instructions: typing.List[MangoInstruction] = []
for instruction_data in response["transaction"]["message"]["instructions"]:
instruction = mango_instruction_from_response(
context, accounts, instruction_data
)
if instruction is not None:
instructions += [instruction]
group_name = (
context.lookup_group_name(instructions[0].group)
if len(instructions) > 0
else "No Group"
)
timestamp = datetime_from_timestamp(response["blockTime"])
signatures = response["transaction"]["signatures"]
raw_messages = response["meta"]["logMessages"]
messages = expand_log_messages(raw_messages)
pre_token_balances = list(
map(
lambda bal: balance_to_token_value(accounts, bal),
response["meta"]["preTokenBalances"],
)
)
post_token_balances = list(
map(
lambda bal: balance_to_token_value(accounts, bal),
response["meta"]["postTokenBalances"],
)
)
return TransactionScout(
timestamp,
signatures,
succeeded,
group_name,
accounts,
instructions,
messages,
pre_token_balances,
post_token_balances,
)
except Exception as exception:
signature = "Unknown"
if (
response
and ("transaction" in response)
and ("signatures" in response["transaction"])
and len(response["transaction"]["signatures"]) > 0
):
signature = ", ".join(response["transaction"]["signatures"])
raise Exception(
f"Exception fetching transaction '{signature}' - {traceback.format_exc()}",
exception,
)
def __str__(self) -> str:
def format_tokens(
account_token_values: typing.Sequence[OwnedInstrumentValue],
) -> str:
if len(account_token_values) == 0:
return "None"
return "\n ".join([f"{atv}" for atv in account_token_values])
instruction_names = ", ".join(
[ins.instruction_type.name for ins in self.instructions]
)
signatures = indent_item_by(indent_collection_as_str(self.signatures), 1)
accounts = indent_item_by(indent_collection_as_str(self.accounts), 1)
messages = indent_item_by(indent_collection_as_str(self.messages), 1)
instructions = indent_item_by(indent_collection_as_str(self.instructions), 1)
changes = OwnedInstrumentValue.changes(
self.pre_token_balances, self.post_token_balances
)
tokens_in = format_tokens(self.pre_token_balances)
tokens_out = format_tokens(self.post_token_balances)
token_changes = format_tokens(changes)
return f"""« TransactionScout {self.timestamp}: {instruction_names}
Sender:
{self.sender}
Succeeded:
{self.succeeded}
Group:
{self.group_name} [{self.group}]
Signatures:
{signatures}
Instructions:
{instructions}
Accounts:
{accounts}
Messages:
{messages}
Tokens In:
{tokens_in}
Tokens Out:
{tokens_out}
Token Changes:
{token_changes}
»"""
def __repr__(self) -> str:
return f"{self}"
# # 🥭 fetch_all_recent_transaction_signatures function
#
def fetch_all_recent_transaction_signatures(context: Context) -> typing.Sequence[str]:
all_fetched = False
before = None
signature_results: typing.List[str] = []
while not all_fetched:
signatures = context.client.get_confirmed_signatures_for_address2(
context.group_address, before=before
)
signature_results += signatures
if len(signatures) == 0:
all_fetched = True
else:
before = signature_results[-1]
return signature_results
def mango_instruction_from_response(
context: Context,
all_accounts: typing.Sequence[PublicKey],
instruction_data: typing.Dict[str, typing.Any],
) -> typing.Optional["MangoInstruction"]:
program_account_index = instruction_data["programIdIndex"]
program_account: PublicKey = all_accounts[program_account_index]
if program_account != context.mango_program_address:
# 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(
program_account, instruction_type, decoded, parsed, accounts
)