mango-explorer/TransactionScout.ipynb

375 lines
17 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"cells": [
{
"cell_type": "markdown",
"id": "wrong-chosen",
"metadata": {},
"source": [
"# ⚠ Warning\n",
"\n",
"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.\n",
"\n",
"[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gl/OpinionatedGeek%2Fmango-explorer/HEAD?filepath=TransactionScout.ipynb) _🏃 To run this notebook press the ⏩ icon in the toolbar above._\n",
"\n",
"[🥭 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)"
]
},
{
"cell_type": "markdown",
"id": "optimum-battle",
"metadata": {},
"source": [
"# 🥭 TransactionScount\n",
"\n",
"This notebook tries to show details of historical transactions.\n",
"\n",
"It fetches the data from Solana, parses it, and then prints it.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "restricted-matthew",
"metadata": {
"jupyter": {
"source_hidden": true
}
},
"outputs": [],
"source": [
"import base58\n",
"import datetime\n",
"import logging\n",
"import typing\n",
"\n",
"logging.getLogger().setLevel(logging.ERROR)\n",
"\n",
"from decimal import Decimal\n",
"from solana.publickey import PublicKey\n",
"\n",
"from BaseModel import InstructionType, OwnedTokenValue, TokenLookup, TokenValue\n",
"from Context import Context, default_context\n",
"from Layouts import InstructionParsersByVariant, MANGO_INSTRUCTION_VARIANT_FINDER\n"
]
},
{
"cell_type": "markdown",
"id": "given-latex",
"metadata": {},
"source": [
"# TransactionScout class"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "innocent-bookmark",
"metadata": {},
"outputs": [],
"source": [
"class TransactionScout:\n",
" def __init__(self, timestamp: datetime.datetime, signatures: typing.List[str],\n",
" accounts: typing.List[PublicKey], instructions: typing.List[typing.Any],\n",
" messages: typing.List[str], pre_token_balances: typing.List[OwnedTokenValue],\n",
" post_token_balances: typing.List[OwnedTokenValue]):\n",
" self.timestamp: datetime.datetime = timestamp\n",
" self.signatures: typing.List[str] = signatures\n",
" self.accounts: typing.List[PublicKey] = accounts\n",
" self.instructions: typing.List[typing.Any] = instructions\n",
" self.messages: typing.List[str] = messages\n",
" self.pre_token_balances: typing.List[OwnedTokenValue] = pre_token_balances\n",
" self.post_token_balances: typing.List[OwnedTokenValue] = post_token_balances\n",
"\n",
" @property\n",
" def summary(self) -> str:\n",
" instructions = \", \".join([InstructionType(int(ins.variant)).name for ins in self.instructions])\n",
" return f\"« TransactionScout [{self.timestamp}] {instructions}\\n {self.signatures} »\"\n",
"\n",
" @property\n",
" def sender(self) -> PublicKey:\n",
" return self.accounts[0]\n",
"\n",
" def has_any_instruction_of_type(self, instruction_type: InstructionType) -> bool:\n",
" return any(map(lambda ins: ins.variant == int(instruction_type), self.instructions))\n",
"\n",
" def _instruction_renderer(self, instruction) -> str:\n",
" instruction_type = InstructionType(int(instruction.variant))\n",
" additional_data = \"\"\n",
" if instruction_type == InstructionType.InitMangoGroup:\n",
" pass\n",
" elif instruction_type == InstructionType.InitMarginAccount:\n",
" pass\n",
" elif instruction_type == InstructionType.Deposit:\n",
" additional_data = f\"quantity: {instruction.quantity}\"\n",
" elif instruction_type == InstructionType.Withdraw:\n",
" additional_data = f\"quantity: {instruction.quantity}\"\n",
" elif instruction_type == InstructionType.Borrow:\n",
" additional_data = f\"quantity: {instruction.quantity}, token index: {instruction.token_index}\"\n",
" elif instruction_type == InstructionType.SettleBorrow:\n",
" additional_data = f\"quantity: {instruction.quantity}, token index: {instruction.token_index}\"\n",
" elif instruction_type == InstructionType.Liquidate:\n",
" additional_data = f\"deposit quantities: {instruction.deposit_quantities}\"\n",
" elif instruction_type == InstructionType.DepositSrm:\n",
" additional_data = f\"quantity: {instruction.quantity}\"\n",
" elif instruction_type == InstructionType.WithdrawSrm:\n",
" additional_data = f\"quantity: {instruction.quantity}\"\n",
" elif instruction_type == InstructionType.PlaceOrder:\n",
" pass\n",
" elif instruction_type == InstructionType.SettleFunds:\n",
" pass\n",
" elif instruction_type == InstructionType.CancelOrder:\n",
" pass\n",
" elif instruction_type == InstructionType.CancelOrderByClientId:\n",
" additional_data = f\"client ID: {instruction.client_id}\"\n",
" elif instruction_type == InstructionType.ChangeBorrowLimit:\n",
" additional_data = f\"borrow limit: {instruction.borrow_limit}, token index: {instruction.token_index}\"\n",
" elif instruction_type == InstructionType.PlaceAndSettle:\n",
" pass\n",
" elif instruction_type == InstructionType.ForceCancelOrders:\n",
" additional_data = f\"limit: {instruction.limit}\"\n",
" elif instruction_type == InstructionType.PartialLiquidate:\n",
" additional_data = f\"max deposit: {instruction.max_deposit}\"\n",
"\n",
" parameters = additional_data or \"None\"\n",
" return f\"« Instruction [{instruction_type.name}]: {parameters} »\"\n",
"\n",
" @staticmethod\n",
" def load_if_available(context: Context, signature: str) -> typing.Optional[\"TransactionScout\"]:\n",
" transaction_response = context.client.get_confirmed_transaction(signature)\n",
" transaction_details = context.unwrap_or_raise_exception(transaction_response)\n",
" if transaction_details is None:\n",
" return None\n",
" return TransactionScout.from_transaction_response(context, transaction_details)\n",
"\n",
" @staticmethod\n",
" def load(context: Context, signature: str) -> \"TransactionScout\":\n",
" tx = TransactionScout.load_if_available(context, signature)\n",
" if tx is None:\n",
" raise Exception(f\"Transaction '{signature}' not found.\")\n",
" return tx\n",
"\n",
" @staticmethod\n",
" def from_transaction_response(context: Context, response) -> \"TransactionScout\":\n",
" def instruction_parser(data: str) -> typing.Tuple[InstructionType, typing.Any]:\n",
" decoded = base58.b58decode(data)\n",
" initial = MANGO_INSTRUCTION_VARIANT_FINDER.parse(decoded)\n",
" parser = InstructionParsersByVariant[initial.variant]\n",
" return parser.parse(decoded)\n",
"\n",
" def balance_to_token_value(accounts: typing.List[PublicKey], balance: typing.Dict) -> OwnedTokenValue:\n",
" mint = PublicKey(balance[\"mint\"])\n",
" account = accounts[balance[\"accountIndex\"]]\n",
" amount = Decimal(balance[\"uiTokenAmount\"][\"amount\"])\n",
" decimals = Decimal(balance[\"uiTokenAmount\"][\"decimals\"])\n",
" divisor = Decimal(10) ** decimals\n",
" value = amount / divisor\n",
" token = TokenLookup.find_by_mint(context, mint)\n",
" return OwnedTokenValue(account, TokenValue(token, value))\n",
"\n",
" try:\n",
" instructions = []\n",
" for instruction in response[\"transaction\"][\"message\"][\"instructions\"]:\n",
" parsed = instruction_parser(instruction[\"data\"])\n",
" instructions += [parsed]\n",
" timestamp = datetime.datetime.fromtimestamp(response[\"blockTime\"])\n",
" accounts = list(map(PublicKey, response[\"transaction\"][\"message\"][\"accountKeys\"]))\n",
" signatures = response[\"transaction\"][\"signatures\"]\n",
" messages = response[\"meta\"][\"logMessages\"]\n",
" pre_token_balances = list(map(lambda bal: balance_to_token_value(accounts, bal), response[\"meta\"][\"preTokenBalances\"]))\n",
" post_token_balances = list(map(lambda bal: balance_to_token_value(accounts, bal), response[\"meta\"][\"postTokenBalances\"]))\n",
" return TransactionScout(timestamp,\n",
" signatures,\n",
" accounts,\n",
" instructions,\n",
" messages,\n",
" pre_token_balances,\n",
" post_token_balances)\n",
" except Exception as exception:\n",
" signature = \"Unknown\"\n",
" if response and (\"transaction\" in response) and (\"signatures\" in response[\"transaction\"]) and len(response[\"transaction\"][\"signatures\"]) > 0:\n",
" signature = \", \".join(response[\"transaction\"][\"signatures\"])\n",
" print(response)\n",
" raise Exception(f\"Exception fetching transaction '{signature}'\", exception)\n",
"\n",
" def __str__(self) -> str:\n",
" def format_tokens(account_token_values: typing.List[OwnedTokenValue]) -> str:\n",
" if len(account_token_values) == 0:\n",
" return \"None\"\n",
" return \"\\n \".join([f\"{atv}\" for atv in account_token_values])\n",
"\n",
" instruction_names = \", \".join([InstructionType(int(ins.variant)).name for ins in self.instructions])\n",
" signatures = \"\\n \".join(self.signatures)\n",
" accounts = \"\\n \".join([f\"{acc}\" for acc in self.accounts])\n",
" messages = \"\\n \".join(self.messages)\n",
" instructions = \"\\n \".join(map(self._instruction_renderer, self.instructions))\n",
" changes = OwnedTokenValue.changes(self.pre_token_balances, self.post_token_balances)\n",
" tokens_in = format_tokens(self.pre_token_balances)\n",
" tokens_out = format_tokens(self.post_token_balances)\n",
" token_changes = format_tokens(changes)\n",
" return f\"\"\"« TransactionScout {self.timestamp}: {instruction_names}\n",
" Sender:\n",
" {self.sender}\n",
" Signatures:\n",
" {signatures}\n",
" Instructions:\n",
" {instructions}\n",
" Accounts:\n",
" {accounts}\n",
" Messages:\n",
" {messages}\n",
" Tokens In:\n",
" {tokens_in}\n",
" Tokens Out:\n",
" {tokens_out}\n",
" Token Changes:\n",
" {token_changes}\n",
"»\"\"\"\n",
"\n",
" def __repr__(self) -> str:\n",
" return f\"{self}\"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "rural-accused",
"metadata": {},
"outputs": [],
"source": [
"def fetch_all_recent_transaction_signatures(in_the_last: datetime.timedelta = datetime.timedelta(days=1)) -> typing.List[str]:\n",
" now = datetime.datetime.now()\n",
" recency_cutoff = now - in_the_last\n",
" recency_cutoff_timestamp = recency_cutoff.timestamp()\n",
"\n",
" all_fetched = False\n",
" before = None\n",
" signature_results = []\n",
" while not all_fetched:\n",
" signature_response = default_context.client.get_confirmed_signature_for_address2(default_context.group_id, before=before)\n",
" signature_result = default_context.unwrap_or_raise_exception(signature_response)\n",
" signature_results += signature_result\n",
" if (len(signature_result) == 0) or (signature_result[-1][\"blockTime\"] < recency_cutoff_timestamp):\n",
" all_fetched = True\n",
" before = signature_results[-1][\"signature\"]\n",
"\n",
" recent = [result[\"signature\"] for result in signature_results if result[\"blockTime\"] > recency_cutoff_timestamp]\n",
" return recent\n"
]
},
{
"cell_type": "markdown",
"id": "metallic-freeze",
"metadata": {},
"source": [
"# 🏃 Running\n",
"\n",
"You can run the following cells to examine any Mango transaction.\n",
"\n",
"Enter the signature of the transaction you want to examine in the value for `TRANSACTION_TO_VIEW` in the box below, between the double-quote marks. Then run the notebook by choosing 'Run > Run All Cells' from the notebook menu at the top of the page.\n",
"\n",
"Alternatively you can uncomment the `rx` code to run through all recent Mango Solana transactions, printing a summary or (if it's a `PartialLiquidate` transaction) the full details.\n",
"\n",
"**NOTE**: Transactions disappear from most servers within a few hours. It is often not possible to go too far back searching for transactions or retrieving their data."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "greatest-electron",
"metadata": {},
"outputs": [],
"source": [
"TRANSACTION_TO_VIEW = \"\""
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "honest-blend",
"metadata": {},
"outputs": [],
"source": [
"if __name__ == \"__main__\":\n",
" if TRANSACTION_TO_VIEW != \"\":\n",
" print(TransactionScout.load(default_context, TRANSACTION_TO_VIEW))\n",
"\n",
"# import rx\n",
"# import rx.operators as ops\n",
"# from Observables import debug_print_item, PrintingObserverSubscriber\n",
"\n",
"# signatures = fetch_all_recent_transaction_signatures()\n",
"# rx.from_(signatures).pipe(\n",
"# # ops.map(debug_print_item(\"Signature:\")),\n",
"# ops.map(lambda signature: TransactionScout.load_if_available(default_context, signature)),\n",
"# ops.filter(lambda item: item is not None),\n",
"# # ops.filter(lambda item: item.has_any_instruction_of_type(InstructionType.PartialLiquidate)),\n",
"# ops.take(5),\n",
"# ).subscribe(PrintingObserverSubscriber(True))\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.6"
},
"toc": {
"base_numbering": 1,
"nav_menu": {},
"number_sections": true,
"sideBar": true,
"skip_h1_title": false,
"title_cell": "Table of Contents",
"title_sidebar": "Contents",
"toc_cell": false,
"toc_position": {},
"toc_section_display": true,
"toc_window_display": true
},
"varInspector": {
"cols": {
"lenName": 16,
"lenType": 16,
"lenVar": 40
},
"kernels_config": {
"python": {
"delete_cmd_postfix": "",
"delete_cmd_prefix": "del ",
"library": "var_list.py",
"varRefreshCmd": "print(var_dic_list())"
},
"r": {
"delete_cmd_postfix": ") ",
"delete_cmd_prefix": "rm(",
"library": "var_list.r",
"varRefreshCmd": "cat(var_dic_list()) "
}
},
"types_to_exclude": [
"module",
"function",
"builtin_function_or_method",
"instance",
"_Feature"
],
"window_display": false
}
},
"nbformat": 4,
"nbformat_minor": 5
}