mango-explorer/Instructions.ipynb

615 lines
30 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": "corresponding-metabolism",
"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=Liquidation.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": "hollywood-victory",
"metadata": {},
"source": [
"# 🥭 Instructions\n",
"\n",
"This notebook contains the low-level `InstructionBuilder`s that build the raw instructions to send to Solana."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "disturbed-washer",
"metadata": {
"jupyter": {
"source_hidden": true
}
},
"outputs": [],
"source": [
"import abc\n",
"import logging\n",
"import struct\n",
"import typing\n",
"\n",
"from decimal import Decimal\n",
"from pyserum.market import Market\n",
"from solana.publickey import PublicKey\n",
"from solana.transaction import AccountMeta, TransactionInstruction\n",
"from solana.sysvar import SYSVAR_CLOCK_PUBKEY\n",
"from spl.token.constants import TOKEN_PROGRAM_ID\n",
"\n",
"from BaseModel import BasketToken, Group, MarginAccount, MarketMetadata, TokenAccount, TokenValue\n",
"from Context import Context\n",
"from Layouts import FORCE_CANCEL_ORDERS, PARTIAL_LIQUIDATE\n",
"from Wallet import Wallet\n"
]
},
{
"cell_type": "markdown",
"id": "different-philosophy",
"metadata": {},
"source": [
"# InstructionBuilder class\n",
"\n",
"An abstract base class for all our `InstructionBuilder`s."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "secret-eugene",
"metadata": {},
"outputs": [],
"source": [
"class InstructionBuilder(metaclass=abc.ABCMeta):\n",
" def __init__(self, context: Context):\n",
" self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n",
" self.context = context\n",
"\n",
" @abc.abstractmethod\n",
" def build(self) -> TransactionInstruction:\n",
" raise NotImplementedError(\"InstructionBuilder.build() is not implemented on the base class.\")\n",
"\n",
" def __repr__(self) -> str:\n",
" return f\"{self}\"\n"
]
},
{
"cell_type": "markdown",
"id": "accessory-drunk",
"metadata": {},
"source": [
"# ForceCancelOrdersInstructionBuilder class"
]
},
{
"cell_type": "markdown",
"id": "touched-consideration",
"metadata": {},
"source": [
"## Rust Interface\n",
"\n",
"This is what the `force_cancel_orders` instruction looks like in the [Mango Rust](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs) code:\n",
"```\n",
"pub fn force_cancel_orders(\n",
" program_id: &Pubkey,\n",
" mango_group_pk: &Pubkey,\n",
" liqor_pk: &Pubkey,\n",
" liqee_margin_account_acc: &Pubkey,\n",
" base_vault_pk: &Pubkey,\n",
" quote_vault_pk: &Pubkey,\n",
" spot_market_pk: &Pubkey,\n",
" bids_pk: &Pubkey,\n",
" asks_pk: &Pubkey,\n",
" signer_pk: &Pubkey,\n",
" dex_event_queue_pk: &Pubkey,\n",
" dex_base_pk: &Pubkey,\n",
" dex_quote_pk: &Pubkey,\n",
" dex_signer_pk: &Pubkey,\n",
" dex_prog_id: &Pubkey,\n",
" open_orders_pks: &[Pubkey],\n",
" oracle_pks: &[Pubkey],\n",
" limit: u8\n",
") -> Result<Instruction, ProgramError>\n",
"```"
]
},
{
"cell_type": "markdown",
"id": "blank-biodiversity",
"metadata": {},
"source": [
"## Client API call\n",
"\n",
"This is how it is built using the Mango Markets client API:\n",
"```\n",
" const keys = [\n",
" { isSigner: false, isWritable: true, pubkey: mangoGroup },\n",
" { isSigner: true, isWritable: false, pubkey: liqor },\n",
" { isSigner: false, isWritable: true, pubkey: liqeeMarginAccount },\n",
" { isSigner: false, isWritable: true, pubkey: baseVault },\n",
" { isSigner: false, isWritable: true, pubkey: quoteVault },\n",
" { isSigner: false, isWritable: true, pubkey: spotMarket },\n",
" { isSigner: false, isWritable: true, pubkey: bids },\n",
" { isSigner: false, isWritable: true, pubkey: asks },\n",
" { isSigner: false, isWritable: false, pubkey: signerKey },\n",
" { isSigner: false, isWritable: true, pubkey: dexEventQueue },\n",
" { isSigner: false, isWritable: true, pubkey: dexBaseVault },\n",
" { isSigner: false, isWritable: true, pubkey: dexQuoteVault },\n",
" { isSigner: false, isWritable: false, pubkey: dexSigner },\n",
" { isSigner: false, isWritable: false, pubkey: TOKEN_PROGRAM_ID },\n",
" { isSigner: false, isWritable: false, pubkey: dexProgramId },\n",
" { isSigner: false, isWritable: false, pubkey: SYSVAR_CLOCK_PUBKEY },\n",
" ...openOrders.map((pubkey) => ({\n",
" isSigner: false,\n",
" isWritable: true,\n",
" pubkey,\n",
" })),\n",
" ...oracles.map((pubkey) => ({\n",
" isSigner: false,\n",
" isWritable: false,\n",
" pubkey,\n",
" })),\n",
" ];\n",
"\n",
" const data = encodeMangoInstruction({ ForceCancelOrders: { limit } });\n",
" return new TransactionInstruction({ keys, data, programId });\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "physical-gentleman",
"metadata": {},
"outputs": [],
"source": [
"class ForceCancelOrdersInstructionBuilder(InstructionBuilder):\n",
" # We can create up to a maximum of max_instructions instructions. I'm not sure of the reason \n",
" # for this threshold but it's what's in the original liquidator source code and I'm assuming\n",
" # it's there for a good reason.\n",
" max_instructions: int = 10\n",
"\n",
" # We cancel up to max_cancels_per_instruction orders with each instruction.\n",
" max_cancels_per_instruction: int = 5\n",
"\n",
" def __init__(self, context: Context, group: Group, wallet: Wallet, margin_account: MarginAccount, market_metadata: MarketMetadata, market: Market, oracles: typing.List[PublicKey], dex_signer: PublicKey):\n",
" super().__init__(context)\n",
" self.group = group\n",
" self.wallet = wallet\n",
" self.margin_account = margin_account\n",
" self.market_metadata = market_metadata\n",
" self.market = market\n",
" self.oracles = oracles\n",
" self.dex_signer = dex_signer\n",
"\n",
" def build(self) -> TransactionInstruction:\n",
" transaction = TransactionInstruction(\n",
" keys=[\n",
" AccountMeta(is_signer=False, is_writable=True, pubkey=self.group.address),\n",
" AccountMeta(is_signer=True, is_writable=False, pubkey=self.wallet.address),\n",
" AccountMeta(is_signer=False, is_writable=True, pubkey=self.margin_account.address),\n",
" AccountMeta(is_signer=False, is_writable=True, pubkey=self.market_metadata.base.vault),\n",
" AccountMeta(is_signer=False, is_writable=True, pubkey=self.market_metadata.quote.vault),\n",
" AccountMeta(is_signer=False, is_writable=True, pubkey=self.market_metadata.spot),\n",
" AccountMeta(is_signer=False, is_writable=True, pubkey=self.market.state.bids()),\n",
" AccountMeta(is_signer=False, is_writable=True, pubkey=self.market.state.asks()),\n",
" AccountMeta(is_signer=False, is_writable=False, pubkey=self.group.signer_key),\n",
" AccountMeta(is_signer=False, is_writable=True, pubkey=self.market.state.event_queue()),\n",
" AccountMeta(is_signer=False, is_writable=True, pubkey=self.market.state.base_vault()),\n",
" AccountMeta(is_signer=False, is_writable=True, pubkey=self.market.state.quote_vault()),\n",
" AccountMeta(is_signer=False, is_writable=False, pubkey=self.dex_signer),\n",
" AccountMeta(is_signer=False, is_writable=False, pubkey=TOKEN_PROGRAM_ID),\n",
" AccountMeta(is_signer=False, is_writable=False, pubkey=self.context.dex_program_id),\n",
" AccountMeta(is_signer=False, is_writable=False, pubkey=SYSVAR_CLOCK_PUBKEY),\n",
" *list([AccountMeta(is_signer=False, is_writable=True, pubkey=oo_address) for oo_address in self.margin_account.open_orders]),\n",
" *list([AccountMeta(is_signer=False, is_writable=False, pubkey=oracle_address) for oracle_address in self.oracles])\n",
" ],\n",
" program_id=self.context.program_id,\n",
" data=FORCE_CANCEL_ORDERS.build({\"limit\": ForceCancelOrdersInstructionBuilder.max_cancels_per_instruction})\n",
" )\n",
" self.logger.debug(f\"Built transaction: {transaction}\")\n",
" return transaction\n",
"\n",
" @staticmethod\n",
" def from_margin_account_and_market(context: Context, group: Group, wallet: Wallet, margin_account: MarginAccount, market_metadata: MarketMetadata) -> \"ForceCancelOrdersInstructionBuilder\":\n",
" market = market_metadata.fetch_market(context)\n",
" nonce = struct.pack(\"<Q\", market.state.vault_signer_nonce())\n",
" dex_signer = PublicKey.create_program_address([bytes(market_metadata.spot), nonce], context.dex_program_id)\n",
" oracles = list([mkt.oracle for mkt in group.markets])\n",
"\n",
" return ForceCancelOrdersInstructionBuilder(context, group, wallet, margin_account, market_metadata, market, oracles, dex_signer)\n",
"\n",
" @classmethod\n",
" def multiple_instructions_from_margin_account_and_market(cls, context: Context, group: Group, wallet: Wallet, margin_account: MarginAccount, market_metadata: MarketMetadata, at_least_this_many_cancellations: int) -> typing.List[\"ForceCancelOrdersInstructionBuilder\"]:\n",
" logger: logging.Logger = logging.getLogger(cls.__name__)\n",
"\n",
" # We cancel up to max_cancels_per_instruction orders with each instruction, but if\n",
" # we have more than cancel_limit we create more instructions (each handling up to\n",
" # 5 orders)\n",
" calculated_instruction_count = int(at_least_this_many_cancellations / ForceCancelOrdersInstructionBuilder.max_cancels_per_instruction) + 1\n",
"\n",
" # We create a maximum of max_instructions instructions.\n",
" instruction_count = min(calculated_instruction_count, ForceCancelOrdersInstructionBuilder.max_instructions)\n",
"\n",
" instructions: typing.List[ForceCancelOrdersInstructionBuilder] = []\n",
" for counter in range(instruction_count):\n",
" instructions += [ForceCancelOrdersInstructionBuilder.from_margin_account_and_market(context, group, wallet, margin_account, market_metadata)]\n",
"\n",
" logger.debug(f\"Built {len(instructions)} transaction(s).\")\n",
"\n",
" return instructions\n",
"\n",
" def __str__(self) -> str:\n",
" # Print the members out using the Rust parameter order and names.\n",
" return f\"\"\"« ForceCancelOrdersInstructionBuilder:\n",
" program_id: &Pubkey: {self.context.program_id},\n",
" mango_group_pk: &Pubkey: {self.group.address},\n",
" liqor_pk: &Pubkey: {self.wallet.address},\n",
" liqee_margin_account_acc: &Pubkey: {self.margin_account.address},\n",
" base_vault_pk: &Pubkey: {self.market_metadata.base.vault},\n",
" quote_vault_pk: &Pubkey: {self.market_metadata.quote.vault},\n",
" spot_market_pk: &Pubkey: {self.market_metadata.spot},\n",
" bids_pk: &Pubkey: {self.market.state.bids()},\n",
" asks_pk: &Pubkey: {self.market.state.asks()},\n",
" signer_pk: &Pubkey: {self.group.signer_key},\n",
" dex_event_queue_pk: &Pubkey: {self.market.state.event_queue()},\n",
" dex_base_pk: &Pubkey: {self.market.state.base_vault()},\n",
" dex_quote_pk: &Pubkey: {self.market.state.quote_vault()},\n",
" dex_signer_pk: &Pubkey: {self.dex_signer},\n",
" dex_prog_id: &Pubkey: {self.context.dex_program_id},\n",
" open_orders_pks: &[Pubkey]: {self.margin_account.open_orders},\n",
" oracle_pks: &[Pubkey]: {self.oracles},\n",
" limit: u8: {ForceCancelOrdersInstructionBuilder.max_cancels_per_instruction}\n",
"»\"\"\"\n"
]
},
{
"cell_type": "markdown",
"id": "changing-humanity",
"metadata": {},
"source": [
"# LiquidateInstructionBuilder class\n",
"\n",
"This is the `Instruction` we send to Solana to perform the (partial) liquidation.\n",
"\n",
"We take care to pass the proper high-level parameters to the `LiquidateInstructionBuilder` constructor so that `build_transaction()` is straightforward. That tends to push complexities to `from_margin_account_and_market()` though.\n"
]
},
{
"cell_type": "markdown",
"id": "occupational-steam",
"metadata": {},
"source": [
"## Rust Interface\n",
"\n",
"This is what the `partial_liquidate` instruction looks like in the [Mango Rust](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs) code:\n",
"```\n",
"/// Take over a MarginAccount that is below init_coll_ratio by depositing funds\n",
"///\n",
"/// Accounts expected by this instruction (10 + 2 * NUM_MARKETS):\n",
"///\n",
"/// 0. `[writable]` mango_group_acc - MangoGroup that this margin account is for\n",
"/// 1. `[signer]` liqor_acc - liquidator's solana account\n",
"/// 2. `[writable]` liqor_in_token_acc - liquidator's token account to deposit\n",
"/// 3. `[writable]` liqor_out_token_acc - liquidator's token account to withdraw into\n",
"/// 4. `[writable]` liqee_margin_account_acc - MarginAccount of liquidatee\n",
"/// 5. `[writable]` in_vault_acc - Mango vault of in_token\n",
"/// 6. `[writable]` out_vault_acc - Mango vault of out_token\n",
"/// 7. `[]` signer_acc\n",
"/// 8. `[]` token_prog_acc - Token program id\n",
"/// 9. `[]` clock_acc - Clock sysvar account\n",
"/// 10..10+NUM_MARKETS `[]` open_orders_accs - open orders for each of the spot market\n",
"/// 10+NUM_MARKETS..10+2*NUM_MARKETS `[]`\n",
"/// oracle_accs - flux aggregator feed accounts\n",
"```\n",
"\n",
"```\n",
"pub fn partial_liquidate(\n",
" program_id: &Pubkey,\n",
" mango_group_pk: &Pubkey,\n",
" liqor_pk: &Pubkey,\n",
" liqor_in_token_pk: &Pubkey,\n",
" liqor_out_token_pk: &Pubkey,\n",
" liqee_margin_account_acc: &Pubkey,\n",
" in_vault_pk: &Pubkey,\n",
" out_vault_pk: &Pubkey,\n",
" signer_pk: &Pubkey,\n",
" open_orders_pks: &[Pubkey],\n",
" oracle_pks: &[Pubkey],\n",
" max_deposit: u64\n",
") -> Result<Instruction, ProgramError>\n",
"```\n"
]
},
{
"cell_type": "markdown",
"id": "heard-farming",
"metadata": {},
"source": [
"## Client API call\n",
"\n",
"This is how it is built using the Mango Markets client API:\n",
"```\n",
" const keys = [\n",
" { isSigner: false, isWritable: true, pubkey: mangoGroup },\n",
" { isSigner: true, isWritable: false, pubkey: liqor },\n",
" { isSigner: false, isWritable: true, pubkey: liqorInTokenWallet },\n",
" { isSigner: false, isWritable: true, pubkey: liqorOutTokenWallet },\n",
" { isSigner: false, isWritable: true, pubkey: liqeeMarginAccount },\n",
" { isSigner: false, isWritable: true, pubkey: inTokenVault },\n",
" { isSigner: false, isWritable: true, pubkey: outTokenVault },\n",
" { isSigner: false, isWritable: false, pubkey: signerKey },\n",
" { isSigner: false, isWritable: false, pubkey: TOKEN_PROGRAM_ID },\n",
" { isSigner: false, isWritable: false, pubkey: SYSVAR_CLOCK_PUBKEY },\n",
" ...openOrders.map((pubkey) => ({\n",
" isSigner: false,\n",
" isWritable: false,\n",
" pubkey,\n",
" })),\n",
" ...oracles.map((pubkey) => ({\n",
" isSigner: false,\n",
" isWritable: false,\n",
" pubkey,\n",
" })),\n",
" ];\n",
" const data = encodeMangoInstruction({ PartialLiquidate: { maxDeposit } });\n",
"\n",
" return new TransactionInstruction({ keys, data, programId });\n",
"```"
]
},
{
"cell_type": "markdown",
"id": "changed-literacy",
"metadata": {},
"source": [
"## from_margin_account_and_market() function\n",
"\n",
"`from_margin_account_and_market()` merits a bit of explaining.\n",
"\n",
"`from_margin_account_and_market()` takes (among other things) a `Wallet` and a `MarginAccount`. The idea is that the `MarginAccount` has some assets in one token, and some liabilities in some different token.\n",
"\n",
"To liquidate the account, we want to:\n",
"* supply tokens from the `Wallet` in the token currency that has the greatest liability value in the `MarginAccount`\n",
"* receive tokens in the `Wallet` in the token currency that has the greatest asset value in the `MarginAccount`\n",
"\n",
"So we calculate the token currencies from the largest liabilities and assets in the `MarginAccount`, but we use those token types to get the correct `Wallet` accounts.\n",
"* `input_token` is the `BasketToken` of the currency the `Wallet` is _paying_ and the `MarginAccount` is _receiving_ to pay off its largest liability.\n",
"* `output_token` is the `BasketToken` of the currency the `Wallet` is _receiving_ and the `MarginAccount` is _paying_ from its largest asset.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "former-cemetery",
"metadata": {},
"outputs": [],
"source": [
"class LiquidateInstructionBuilder(InstructionBuilder):\n",
" def __init__(self, context: Context, group: Group, wallet: Wallet, margin_account: MarginAccount, oracles: typing.List[PublicKey], input_token: BasketToken, output_token: BasketToken, wallet_input_token_account: TokenAccount, wallet_output_token_account: TokenAccount, maximum_input_amount: Decimal):\n",
" super().__init__(context)\n",
" self.group: Group = group\n",
" self.wallet: Wallet = wallet\n",
" self.margin_account: MarginAccount = margin_account\n",
" self.oracles: typing.List[PublicKey] = oracles\n",
" self.input_token: BasketToken = input_token\n",
" self.output_token: BasketToken = output_token\n",
" self.wallet_input_token_account: TokenAccount = wallet_input_token_account\n",
" self.wallet_output_token_account: TokenAccount = wallet_output_token_account\n",
" self.maximum_input_amount: Decimal = maximum_input_amount\n",
"\n",
" def build(self) -> TransactionInstruction:\n",
" transaction = TransactionInstruction(\n",
" keys=[\n",
" AccountMeta(is_signer=False, is_writable=True, pubkey=self.group.address),\n",
" AccountMeta(is_signer=True, is_writable=False, pubkey=self.wallet.address),\n",
" AccountMeta(is_signer=False, is_writable=True, pubkey=self.wallet_input_token_account.address),\n",
" AccountMeta(is_signer=False, is_writable=True, pubkey=self.wallet_output_token_account.address),\n",
" AccountMeta(is_signer=False, is_writable=True, pubkey=self.margin_account.address),\n",
" AccountMeta(is_signer=False, is_writable=True, pubkey=self.input_token.vault),\n",
" AccountMeta(is_signer=False, is_writable=True, pubkey=self.output_token.vault),\n",
" AccountMeta(is_signer=False, is_writable=False, pubkey=self.group.signer_key),\n",
" AccountMeta(is_signer=False, is_writable=False, pubkey=TOKEN_PROGRAM_ID),\n",
" AccountMeta(is_signer=False, is_writable=False, pubkey=SYSVAR_CLOCK_PUBKEY),\n",
" *list([AccountMeta(is_signer=False, is_writable=True, pubkey=oo_address) for oo_address in self.margin_account.open_orders]),\n",
" *list([AccountMeta(is_signer=False, is_writable=False, pubkey=oracle_address) for oracle_address in self.oracles])\n",
" ],\n",
" program_id=self.context.program_id,\n",
" data=PARTIAL_LIQUIDATE.build({\"max_deposit\": int(self.maximum_input_amount)})\n",
" )\n",
" self.logger.debug(f\"Built transaction: {transaction}\")\n",
" return transaction\n",
"\n",
" @classmethod\n",
" def from_margin_account_and_market(cls, context: Context, group: Group, wallet: Wallet, margin_account: MarginAccount, prices: typing.List[TokenValue]) -> typing.Optional[\"LiquidateInstructionBuilder\"]:\n",
" logger: logging.Logger = logging.getLogger(cls.__name__)\n",
"\n",
" oracles = list([mkt.oracle for mkt in group.markets])\n",
"\n",
" balance_sheets = margin_account.get_priced_balance_sheets(group, prices)\n",
"\n",
" sorted_by_assets = sorted(balance_sheets, key=lambda sheet: sheet.assets, reverse=True)\n",
" sorted_by_liabilities = sorted(balance_sheets, key=lambda sheet: sheet.liabilities, reverse=True)\n",
"\n",
" most_assets = sorted_by_assets[0]\n",
" most_liabilities = sorted_by_liabilities[0]\n",
" if most_assets.token == most_liabilities.token:\n",
" # If there's a weirdness where the account with the biggest assets is also the one\n",
" # with the biggest liabilities, pick the next-best one by assets.\n",
" logger.info(f\"Switching asset token from {most_assets.token.name} to {sorted_by_assets[1].token.name} because {most_liabilities.token.name} is the token with most liabilities.\")\n",
" most_assets = sorted_by_assets[1]\n",
"\n",
" logger.info(f\"Most assets: {most_assets}\")\n",
" logger.info(f\"Most liabilities: {most_liabilities}\")\n",
"\n",
" most_assets_basket_token = BasketToken.find_by_token(group.basket_tokens, most_assets.token)\n",
" most_liabilities_basket_token = BasketToken.find_by_token(group.basket_tokens, most_liabilities.token)\n",
" logger.info(f\"Most assets basket token: {most_assets_basket_token}\")\n",
" logger.info(f\"Most liabilities basket token: {most_liabilities_basket_token}\")\n",
"\n",
" if most_assets.value == Decimal(0):\n",
" logger.warning(f\"Margin account {margin_account.address} has no assets to take.\")\n",
" return None\n",
"\n",
" if most_liabilities.value == Decimal(0):\n",
" logger.warning(f\"Margin account {margin_account.address} has no liabilities to fund.\")\n",
" return None\n",
"\n",
" wallet_input_token_account = TokenAccount.fetch_largest_for_owner_and_token(context, wallet.address, most_liabilities.token)\n",
" if wallet_input_token_account is None:\n",
" raise Exception(f\"Could not load wallet input token account for mint '{most_liabilities.token.mint}'\")\n",
"\n",
" if wallet_input_token_account.amount == Decimal(0):\n",
" logger.warning(f\"Wallet token account {wallet_input_token_account.address} has no tokens to send that could fund a liquidation.\")\n",
" return None\n",
"\n",
" wallet_output_token_account = TokenAccount.fetch_largest_for_owner_and_token(context, wallet.address, most_assets.token)\n",
" if wallet_output_token_account is None:\n",
" raise Exception(f\"Could not load wallet output token account for mint '{most_assets.token.mint}'\")\n",
"\n",
" return LiquidateInstructionBuilder(context, group, wallet, margin_account, oracles,\n",
" most_liabilities_basket_token, most_assets_basket_token,\n",
" wallet_input_token_account,\n",
" wallet_output_token_account,\n",
" wallet_input_token_account.amount)\n",
"\n",
" def __str__(self) -> str:\n",
" # Print the members out using the Rust parameter order and names.\n",
" return f\"\"\"« LiquidateInstructionBuilder:\n",
" program_id: &Pubkey: {self.context.program_id},\n",
" mango_group_pk: &Pubkey: {self.group.address},\n",
" liqor_pk: &Pubkey: {self.wallet.address},\n",
" liqor_in_token_pk: &Pubkey: {self.wallet_input_token_account.address},\n",
" liqor_out_token_pk: &Pubkey: {self.wallet_output_token_account.address},\n",
" liqee_margin_account_acc: &Pubkey: {self.margin_account.address},\n",
" in_vault_pk: &Pubkey: {self.input_token.vault},\n",
" out_vault_pk: &Pubkey: {self.output_token.vault},\n",
" signer_pk: &Pubkey: {self.group.signer_key},\n",
" open_orders_pks: &[Pubkey]: {self.margin_account.open_orders},\n",
" oracle_pks: &[Pubkey]: {self.oracles},\n",
" max_deposit: u64: : {self.maximum_input_amount}\n",
"»\"\"\"\n"
]
},
{
"cell_type": "markdown",
"id": "traditional-assembly",
"metadata": {},
"source": [
"# 🏃 Running"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "victorian-character",
"metadata": {},
"outputs": [],
"source": [
"if __name__ == \"__main__\":\n",
" logging.getLogger().setLevel(logging.INFO)\n",
"\n",
" from Wallet import default_wallet\n",
" if default_wallet is None:\n",
" print(\"No default wallet file available.\")\n",
" else:\n",
" # Build and print a ForceCancelOrdersInstructionBuilder and LiquidateInstructionBuilder\n",
" # for the loaded Wallet and the margin account for the loaded wallet.\n",
" #\n",
" # It doesn't make a lot of sense to do this in real life, but it should load and show\n",
" # the proper values when printing.\n",
" from Context import default_context\n",
"\n",
" group = Group.load(default_context)\n",
" my_margin_accounts = MarginAccount.load_all_for_owner(default_context, default_wallet.address, group)\n",
" margin_account = my_margin_accounts[0]\n",
" market_metadata = group.markets[0]\n",
"\n",
" force_cancel = ForceCancelOrdersInstructionBuilder.from_margin_account_and_market(default_context, group, default_wallet, margin_account, market_metadata)\n",
" force_cancel_instruction = force_cancel.build()\n",
" print(\"ForceCancelOrdersInstruction\", force_cancel_instruction, \"Data:\", \" \".join(f\"{x:02x}\" for x in force_cancel_instruction.data))\n",
"\n",
" prices = group.fetch_token_prices()\n",
"\n",
" liquidate = LiquidateInstructionBuilder.from_margin_account_and_market(default_context, group, default_wallet, margin_account, prices)\n",
" if liquidate is not None:\n",
" liquidate_instruction = liquidate.build()\n",
" print(\"LiquidateInstruction\", liquidate_instruction, \"Data:\", \" \".join(f\"{x:02x}\" for x in liquidate_instruction.data))\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
}