mango-explorer/mango/transactionscout.py

264 lines
12 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 datetime
import logging
import traceback
import typing
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 .ownedtokenvalue import OwnedTokenValue
from .tokenvalue import TokenValue
# # 🥭 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.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[OwnedTokenValue],
post_token_balances: typing.Sequence[OwnedTokenValue]):
self.timestamp: datetime.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[OwnedTokenValue] = pre_token_balances
self.post_token_balances: typing.Sequence[OwnedTokenValue] = 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 = OwnedTokenValue.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 += [OwnedTokenValue.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 += [OwnedTokenValue.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
@property
def group(self) -> PublicKey:
return self.instructions[0].group
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) -> "TransactionScout":
def balance_to_token_value(accounts: typing.Sequence[PublicKey], balance: typing.Dict) -> OwnedTokenValue:
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.token_lookup.find_by_mint_or_raise(mint)
return OwnedTokenValue(account, TokenValue(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)
timestamp = datetime.datetime.fromtimestamp(response["blockTime"])
signatures = response["transaction"]["signatures"]
messages = response["meta"]["logMessages"]
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[OwnedTokenValue]) -> 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 = "\n ".join(self.signatures)
accounts = "\n ".join([f"{acc}" for acc in self.accounts])
messages = "\n ".join(self.messages)
instructions = "\n ".join([f"{ins}" for ins in self.instructions])
changes = OwnedTokenValue.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) -> typing.Optional["MangoInstruction"]:
program_account_index = instruction_data["programIdIndex"]
if all_accounts[program_account_index] != 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(instruction_type, parsed, accounts)