mango-explorer/mango/instructions.py

447 lines
21 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 abc
import logging
import struct
import typing
from decimal import Decimal
from pyserum.market import Market
from solana.publickey import PublicKey
from solana.transaction import AccountMeta, TransactionInstruction
from solana.sysvar import SYSVAR_CLOCK_PUBKEY
from spl.token.constants import TOKEN_PROGRAM_ID
from .baskettoken import BasketToken
from .context import Context
from .group import Group
from .layouts import layouts
from .marginaccount import MarginAccount
from .marketmetadata import MarketMetadata
from .tokenaccount import TokenAccount
from .tokenvalue import TokenValue
from .wallet import Wallet
# 🥭 Instructions
#
# This notebook contains the low-level `InstructionBuilder`s that build the raw instructions
# to send to Solana.
#
# # 🥭 InstructionBuilder class
#
# An abstract base class for all our `InstructionBuilder`s.
#
class InstructionBuilder(metaclass=abc.ABCMeta):
def __init__(self, context: Context):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.context = context
@abc.abstractmethod
def build(self) -> TransactionInstruction:
raise NotImplementedError("InstructionBuilder.build() is not implemented on the base class.")
def __repr__(self) -> str:
return f"{self}"
# # 🥭 ForceCancelOrdersInstructionBuilder class
#
#
# ## Rust Interface
#
# 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:
# ```
# pub fn force_cancel_orders(
# program_id: &Pubkey,
# mango_group_pk: &Pubkey,
# liqor_pk: &Pubkey,
# liqee_margin_account_acc: &Pubkey,
# base_vault_pk: &Pubkey,
# quote_vault_pk: &Pubkey,
# spot_market_pk: &Pubkey,
# bids_pk: &Pubkey,
# asks_pk: &Pubkey,
# signer_pk: &Pubkey,
# dex_event_queue_pk: &Pubkey,
# dex_base_pk: &Pubkey,
# dex_quote_pk: &Pubkey,
# dex_signer_pk: &Pubkey,
# dex_prog_id: &Pubkey,
# open_orders_pks: &[Pubkey],
# oracle_pks: &[Pubkey],
# limit: u8
# ) -> Result<Instruction, ProgramError>
# ```
#
# ## Client API call
#
# This is how it is built using the Mango Markets client API:
# ```
# const keys = [
# { isSigner: false, isWritable: true, pubkey: mangoGroup },
# { isSigner: true, isWritable: false, pubkey: liqor },
# { isSigner: false, isWritable: true, pubkey: liqeeMarginAccount },
# { isSigner: false, isWritable: true, pubkey: baseVault },
# { isSigner: false, isWritable: true, pubkey: quoteVault },
# { isSigner: false, isWritable: true, pubkey: spotMarket },
# { isSigner: false, isWritable: true, pubkey: bids },
# { isSigner: false, isWritable: true, pubkey: asks },
# { isSigner: false, isWritable: false, pubkey: signerKey },
# { isSigner: false, isWritable: true, pubkey: dexEventQueue },
# { isSigner: false, isWritable: true, pubkey: dexBaseVault },
# { isSigner: false, isWritable: true, pubkey: dexQuoteVault },
# { isSigner: false, isWritable: false, pubkey: dexSigner },
# { isSigner: false, isWritable: false, pubkey: TOKEN_PROGRAM_ID },
# { isSigner: false, isWritable: false, pubkey: dexProgramId },
# { isSigner: false, isWritable: false, pubkey: SYSVAR_CLOCK_PUBKEY },
# ...openOrders.map((pubkey) => ({
# isSigner: false,
# isWritable: true,
# pubkey,
# })),
# ...oracles.map((pubkey) => ({
# isSigner: false,
# isWritable: false,
# pubkey,
# })),
# ];
#
# const data = encodeMangoInstruction({ ForceCancelOrders: { limit } });
# return new TransactionInstruction({ keys, data, programId });
# ```
#
class ForceCancelOrdersInstructionBuilder(InstructionBuilder):
# We can create up to a maximum of max_instructions instructions. I'm not sure of the reason
# for this threshold but it's what's in the original liquidator source code and I'm assuming
# it's there for a good reason.
max_instructions: int = 10
# We cancel up to max_cancels_per_instruction orders with each instruction.
max_cancels_per_instruction: int = 5
def __init__(self, context: Context, group: Group, wallet: Wallet, margin_account: MarginAccount, market_metadata: MarketMetadata, market: Market, oracles: typing.List[PublicKey], dex_signer: PublicKey):
super().__init__(context)
self.group = group
self.wallet = wallet
self.margin_account = margin_account
self.market_metadata = market_metadata
self.market = market
self.oracles = oracles
self.dex_signer = dex_signer
def build(self) -> TransactionInstruction:
transaction = TransactionInstruction(
keys=[
AccountMeta(is_signer=False, is_writable=True, pubkey=self.group.address),
AccountMeta(is_signer=True, is_writable=False, pubkey=self.wallet.address),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.margin_account.address),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.market_metadata.base.vault),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.market_metadata.quote.vault),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.market_metadata.spot.address),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.market.state.bids()),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.market.state.asks()),
AccountMeta(is_signer=False, is_writable=False, pubkey=self.group.signer_key),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.market.state.event_queue()),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.market.state.base_vault()),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.market.state.quote_vault()),
AccountMeta(is_signer=False, is_writable=False, pubkey=self.dex_signer),
AccountMeta(is_signer=False, is_writable=False, pubkey=TOKEN_PROGRAM_ID),
AccountMeta(is_signer=False, is_writable=False, pubkey=self.context.dex_program_id),
AccountMeta(is_signer=False, is_writable=False, pubkey=SYSVAR_CLOCK_PUBKEY),
*list([AccountMeta(is_signer=False, is_writable=True, pubkey=oo_address)
for oo_address in self.margin_account.open_orders]),
*list([AccountMeta(is_signer=False, is_writable=False, pubkey=oracle_address) for oracle_address in self.oracles])
],
program_id=self.context.program_id,
data=layouts.FORCE_CANCEL_ORDERS.build(
{"limit": ForceCancelOrdersInstructionBuilder.max_cancels_per_instruction})
)
self.logger.debug(f"Built transaction: {transaction}")
return transaction
@staticmethod
def from_margin_account_and_market(context: Context, group: Group, wallet: Wallet, margin_account: MarginAccount, market_metadata: MarketMetadata) -> "ForceCancelOrdersInstructionBuilder":
market = market_metadata.fetch_market(context)
nonce = struct.pack("<Q", market.state.vault_signer_nonce())
dex_signer = PublicKey.create_program_address(
[bytes(market_metadata.spot.address), nonce], context.dex_program_id)
oracles = list([mkt.oracle for mkt in group.markets])
return ForceCancelOrdersInstructionBuilder(context, group, wallet, margin_account, market_metadata, market, oracles, dex_signer)
@classmethod
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"]:
logger: logging.Logger = logging.getLogger(cls.__name__)
# We cancel up to max_cancels_per_instruction orders with each instruction, but if
# we have more than cancel_limit we create more instructions (each handling up to
# 5 orders)
calculated_instruction_count = int(
at_least_this_many_cancellations / ForceCancelOrdersInstructionBuilder.max_cancels_per_instruction) + 1
# We create a maximum of max_instructions instructions.
instruction_count = min(calculated_instruction_count, ForceCancelOrdersInstructionBuilder.max_instructions)
instructions: typing.List[ForceCancelOrdersInstructionBuilder] = []
for counter in range(instruction_count):
instructions += [ForceCancelOrdersInstructionBuilder.from_margin_account_and_market(
context, group, wallet, margin_account, market_metadata)]
logger.debug(f"Built {len(instructions)} transaction(s).")
return instructions
def __str__(self) -> str:
# Print the members out using the Rust parameter order and names.
return f"""« ForceCancelOrdersInstructionBuilder:
program_id: &Pubkey: {self.context.program_id},
mango_group_pk: &Pubkey: {self.group.address},
liqor_pk: &Pubkey: {self.wallet.address},
liqee_margin_account_acc: &Pubkey: {self.margin_account.address},
base_vault_pk: &Pubkey: {self.market_metadata.base.vault},
quote_vault_pk: &Pubkey: {self.market_metadata.quote.vault},
spot_market_pk: &Pubkey: {self.market_metadata.spot.address},
bids_pk: &Pubkey: {self.market.state.bids()},
asks_pk: &Pubkey: {self.market.state.asks()},
signer_pk: &Pubkey: {self.group.signer_key},
dex_event_queue_pk: &Pubkey: {self.market.state.event_queue()},
dex_base_pk: &Pubkey: {self.market.state.base_vault()},
dex_quote_pk: &Pubkey: {self.market.state.quote_vault()},
dex_signer_pk: &Pubkey: {self.dex_signer},
dex_prog_id: &Pubkey: {self.context.dex_program_id},
open_orders_pks: &[Pubkey]: {self.margin_account.open_orders},
oracle_pks: &[Pubkey]: {self.oracles},
limit: u8: {ForceCancelOrdersInstructionBuilder.max_cancels_per_instruction}
»"""
# # 🥭 LiquidateInstructionBuilder class
#
# This is the `Instruction` we send to Solana to perform the (partial) liquidation.
#
# 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.
#
# ## Rust Interface
#
# 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:
# ```
# /// Take over a MarginAccount that is below init_coll_ratio by depositing funds
# ///
# /// Accounts expected by this instruction (10 + 2 * NUM_MARKETS):
# ///
# /// 0. `[writable]` mango_group_acc - MangoGroup that this margin account is for
# /// 1. `[signer]` liqor_acc - liquidator's solana account
# /// 2. `[writable]` liqor_in_token_acc - liquidator's token account to deposit
# /// 3. `[writable]` liqor_out_token_acc - liquidator's token account to withdraw into
# /// 4. `[writable]` liqee_margin_account_acc - MarginAccount of liquidatee
# /// 5. `[writable]` in_vault_acc - Mango vault of in_token
# /// 6. `[writable]` out_vault_acc - Mango vault of out_token
# /// 7. `[]` signer_acc
# /// 8. `[]` token_prog_acc - Token program id
# /// 9. `[]` clock_acc - Clock sysvar account
# /// 10..10+NUM_MARKETS `[]` open_orders_accs - open orders for each of the spot market
# /// 10+NUM_MARKETS..10+2*NUM_MARKETS `[]`
# /// oracle_accs - flux aggregator feed accounts
# ```
#
# ```
# pub fn partial_liquidate(
# program_id: &Pubkey,
# mango_group_pk: &Pubkey,
# liqor_pk: &Pubkey,
# liqor_in_token_pk: &Pubkey,
# liqor_out_token_pk: &Pubkey,
# liqee_margin_account_acc: &Pubkey,
# in_vault_pk: &Pubkey,
# out_vault_pk: &Pubkey,
# signer_pk: &Pubkey,
# open_orders_pks: &[Pubkey],
# oracle_pks: &[Pubkey],
# max_deposit: u64
# ) -> Result<Instruction, ProgramError>
# ```
#
# ## Client API call
#
# This is how it is built using the Mango Markets client API:
# ```
# const keys = [
# { isSigner: false, isWritable: true, pubkey: mangoGroup },
# { isSigner: true, isWritable: false, pubkey: liqor },
# { isSigner: false, isWritable: true, pubkey: liqorInTokenWallet },
# { isSigner: false, isWritable: true, pubkey: liqorOutTokenWallet },
# { isSigner: false, isWritable: true, pubkey: liqeeMarginAccount },
# { isSigner: false, isWritable: true, pubkey: inTokenVault },
# { isSigner: false, isWritable: true, pubkey: outTokenVault },
# { isSigner: false, isWritable: false, pubkey: signerKey },
# { isSigner: false, isWritable: false, pubkey: TOKEN_PROGRAM_ID },
# { isSigner: false, isWritable: false, pubkey: SYSVAR_CLOCK_PUBKEY },
# ...openOrders.map((pubkey) => ({
# isSigner: false,
# isWritable: false,
# pubkey,
# })),
# ...oracles.map((pubkey) => ({
# isSigner: false,
# isWritable: false,
# pubkey,
# })),
# ];
# const data = encodeMangoInstruction({ PartialLiquidate: { maxDeposit } });
#
# return new TransactionInstruction({ keys, data, programId });
# ```
#
# ## from_margin_account_and_market() function
#
# `from_margin_account_and_market()` merits a bit of explaining.
#
# `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.
#
# To liquidate the account, we want to:
# * supply tokens from the `Wallet` in the token currency that has the greatest liability
# value in the `MarginAccount`
# * receive tokens in the `Wallet` in the token currency that has the greatest asset value
# in the `MarginAccount`
#
# 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.
# * `input_token` is the `BasketToken` of the currency the `Wallet` is _paying_ and the
# `MarginAccount` is _receiving_ to pay off its largest liability.
# * `output_token` is the `BasketToken` of the currency the `Wallet` is _receiving_ and the
# `MarginAccount` is _paying_ from its largest asset.
#
class LiquidateInstructionBuilder(InstructionBuilder):
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):
super().__init__(context)
self.group: Group = group
self.wallet: Wallet = wallet
self.margin_account: MarginAccount = margin_account
self.oracles: typing.List[PublicKey] = oracles
self.input_token: BasketToken = input_token
self.output_token: BasketToken = output_token
self.wallet_input_token_account: TokenAccount = wallet_input_token_account
self.wallet_output_token_account: TokenAccount = wallet_output_token_account
self.maximum_input_amount: Decimal = maximum_input_amount
def build(self) -> TransactionInstruction:
transaction = TransactionInstruction(
keys=[
AccountMeta(is_signer=False, is_writable=True, pubkey=self.group.address),
AccountMeta(is_signer=True, is_writable=False, pubkey=self.wallet.address),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.wallet_input_token_account.address),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.wallet_output_token_account.address),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.margin_account.address),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.input_token.vault),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.output_token.vault),
AccountMeta(is_signer=False, is_writable=False, pubkey=self.group.signer_key),
AccountMeta(is_signer=False, is_writable=False, pubkey=TOKEN_PROGRAM_ID),
AccountMeta(is_signer=False, is_writable=False, pubkey=SYSVAR_CLOCK_PUBKEY),
*list([AccountMeta(is_signer=False, is_writable=True, pubkey=oo_address)
for oo_address in self.margin_account.open_orders]),
*list([AccountMeta(is_signer=False, is_writable=False, pubkey=oracle_address) for oracle_address in self.oracles])
],
program_id=self.context.program_id,
data=layouts.PARTIAL_LIQUIDATE.build({"max_deposit": int(self.maximum_input_amount)})
)
self.logger.debug(f"Built transaction: {transaction}")
return transaction
@classmethod
def from_margin_account_and_market(cls, context: Context, group: Group, wallet: Wallet, margin_account: MarginAccount, prices: typing.List[TokenValue]) -> typing.Optional["LiquidateInstructionBuilder"]:
logger: logging.Logger = logging.getLogger(cls.__name__)
oracles = list([mkt.oracle for mkt in group.markets])
balance_sheets = margin_account.get_priced_balance_sheets(group, prices)
sorted_by_assets = sorted(balance_sheets, key=lambda sheet: sheet.assets, reverse=True)
sorted_by_liabilities = sorted(balance_sheets, key=lambda sheet: sheet.liabilities, reverse=True)
most_assets = sorted_by_assets[0]
most_liabilities = sorted_by_liabilities[0]
if most_assets.token == most_liabilities.token:
# If there's a weirdness where the account with the biggest assets is also the one
# with the biggest liabilities, pick the next-best one by assets.
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.")
most_assets = sorted_by_assets[1]
logger.info(f"Most assets: {most_assets}")
logger.info(f"Most liabilities: {most_liabilities}")
most_assets_basket_token = BasketToken.find_by_token(group.basket_tokens, most_assets.token)
most_liabilities_basket_token = BasketToken.find_by_token(group.basket_tokens, most_liabilities.token)
logger.info(f"Most assets basket token: {most_assets_basket_token}")
logger.info(f"Most liabilities basket token: {most_liabilities_basket_token}")
if most_assets.value == Decimal(0):
logger.warning(f"Margin account {margin_account.address} has no assets to take.")
return None
if most_liabilities.value == Decimal(0):
logger.warning(f"Margin account {margin_account.address} has no liabilities to fund.")
return None
wallet_input_token_account = TokenAccount.fetch_largest_for_owner_and_token(
context, wallet.address, most_liabilities.token)
if wallet_input_token_account is None:
raise Exception(f"Could not load wallet input token account for mint '{most_liabilities.token.mint}'")
if wallet_input_token_account.amount == Decimal(0):
logger.warning(
f"Wallet token account {wallet_input_token_account.address} has no tokens to send that could fund a liquidation.")
return None
wallet_output_token_account = TokenAccount.fetch_largest_for_owner_and_token(
context, wallet.address, most_assets.token)
if wallet_output_token_account is None:
raise Exception(f"Could not load wallet output token account for mint '{most_assets.token.mint}'")
return LiquidateInstructionBuilder(context, group, wallet, margin_account, oracles,
most_liabilities_basket_token, most_assets_basket_token,
wallet_input_token_account,
wallet_output_token_account,
wallet_input_token_account.amount)
def __str__(self) -> str:
# Print the members out using the Rust parameter order and names.
return f"""« LiquidateInstructionBuilder:
program_id: &Pubkey: {self.context.program_id},
mango_group_pk: &Pubkey: {self.group.address},
liqor_pk: &Pubkey: {self.wallet.address},
liqor_in_token_pk: &Pubkey: {self.wallet_input_token_account.address},
liqor_out_token_pk: &Pubkey: {self.wallet_output_token_account.address},
liqee_margin_account_acc: &Pubkey: {self.margin_account.address},
in_vault_pk: &Pubkey: {self.input_token.vault},
out_vault_pk: &Pubkey: {self.output_token.vault},
signer_pk: &Pubkey: {self.group.signer_key},
open_orders_pks: &[Pubkey]: {self.margin_account.open_orders},
oracle_pks: &[Pubkey]: {self.oracles},
max_deposit: u64: : {self.maximum_input_amount}
»"""