diff --git a/bin/crank-market b/bin/crank-market index 6150354..1f52876 100755 --- a/bin/crank-market +++ b/bin/crank-market @@ -15,7 +15,7 @@ import mango # nopep8 parser = argparse.ArgumentParser(description="Cranks all openorders in the market.") mango.ContextBuilder.add_command_line_parameters(parser) mango.Wallet.add_command_line_parameters(parser) -parser.add_argument("--market", type=str, required=True, help="market symbol to make market upon (e.g. ETH/USDC)") +parser.add_argument("--market", type=str, required=True, help="market symbol to crank (e.g. ETH/USDC)") parser.add_argument("--limit", type=Decimal, default=Decimal(32), help="maximum number of events to be processed") parser.add_argument("--account-index", type=int, default=0, help="index of the account to use, if more than one available") diff --git a/bin/redeem-mango b/bin/redeem-mango new file mode 100755 index 0000000..116ee7f --- /dev/null +++ b/bin/redeem-mango @@ -0,0 +1,113 @@ +#!/usr/bin/env pyston3 + +import argparse +import logging +import os +import os.path +import sys +import typing + +sys.path.insert(0, os.path.abspath( + os.path.join(os.path.dirname(__file__), '..'))) +import mango # nopep8 + + +def report_accrued(basket_token: mango.AccountBasketBaseToken): + symbol: str = basket_token.token_info.token.symbol + accrued: mango.TokenValue = basket_token.perp_account.mngo_accrued + print(f"Accrued in perp market [{symbol:>5}]: {accrued}") + + +def load_perp_market(context: mango.Context, group: mango.Group, group_basket_market: mango.GroupBasketMarket): + perp_market_details = mango.PerpMarketDetails.load(context, group_basket_market.perp_market_info.address, group) + perp_market = mango.PerpMarket(group_basket_market.perp_market_info.address, group_basket_market.base_token_info.token, + group_basket_market.quote_token_info.token, perp_market_details) + + return perp_market + + +def find_basket_token_in_account(account: mango.Account, token: mango.Token) -> typing.Optional[mango.AccountBasketBaseToken]: + basket_tokens = [in_basket for in_basket in account.basket if in_basket.token_info.token == token] + + if len(basket_tokens) == 0: + return None + else: + return basket_tokens[0] + + +def build_redeem_instruction_for_account(context: mango.Context, wallet: mango.Wallet, group: mango.Group, + mngo: mango.TokenInfo, account: mango.Account, + perp_market: mango.PerpMarket, + basket_token: typing.Optional[mango.AccountBasketBaseToken]): + if (basket_token is None) or basket_token.perp_account.mngo_accrued.value == 0: + return mango.CombinableInstructions.empty() + + report_accrued(basket_token) + redeem = mango.build_redeem_accrued_mango_instructions(context, wallet, perp_market, group, account, mngo) + + return redeem + + +parser = argparse.ArgumentParser(description="redeems accrued MNGO from a Mango account") +mango.ContextBuilder.add_command_line_parameters(parser) +mango.Wallet.add_command_line_parameters(parser) +parser.add_argument("--market", type=str, help="perp market symbol with accrued MNGO (e.g. ETH-PERP)") +parser.add_argument("--all", action="store_true", default=False, + help="redeem all MNGO in all perp markets in the account") +parser.add_argument("--account-index", type=int, default=0, + help="index of the account to use, if more than one available") +parser.add_argument("--wait", action="store_true", default=False, + help="wait until the transaction is confirmed") +args = parser.parse_args() + +logging.getLogger().setLevel(args.log_level) +logging.warning(mango.WARNING_DISCLAIMER_TEXT) + +if (not args.all) and (args.market is None): + raise Exception("Must specify either an individual market (using --market) or use --all for all markets") + +context = mango.ContextBuilder.from_command_line_parameters(args) +wallet = mango.Wallet.from_command_line_parameters_or_raise(args) + +group = mango.Group.load(context, context.group_id) +mngo = group.find_token_info_by_symbol("MNGO") +account = mango.Account.load_for_owner_by_index(context, wallet.address, group, args.account_index) + +signers: mango.CombinableInstructions = mango.CombinableInstructions.from_wallet(wallet) +all_instructions: mango.CombinableInstructions = signers + +if args.all: + for group_basket_market in group.basket: + perp_market = load_perp_market(context, group, group_basket_market) + basket_token = find_basket_token_in_account(account, group_basket_market.base_token_info.token) + all_instructions += build_redeem_instruction_for_account(context, + wallet, group, mngo, account, perp_market, basket_token) +else: + market_symbol = args.market.upper() + market = context.market_lookup.find_by_symbol(market_symbol) + if market is None: + raise Exception(f"Could not find market {market_symbol}") + + perp_market = mango.ensure_market_loaded(context, market) + if not isinstance(perp_market, mango.PerpMarket): + raise Exception(f"Market {market_symbol} is not a perp market") + + basket_token = find_basket_token_in_account(account, perp_market.base) + all_instructions += build_redeem_instruction_for_account(context, + wallet, group, mngo, account, perp_market, basket_token) + +transaction_ids = all_instructions.execute(context) +print("Transaction IDs:", transaction_ids) + +if args.wait: + context.client.wait_for_confirmation(transaction_ids) + reloaded_account = mango.Account.load_for_owner_by_index(context, wallet.address, group, args.account_index) + if args.all: + for group_basket_market in group.basket: + basket_token = find_basket_token_in_account(reloaded_account, group_basket_market.base_token_info.token) + if basket_token is not None: + report_accrued(basket_token) + else: + basket_token = find_basket_token_in_account(reloaded_account, perp_market.base) + if basket_token is not None: + report_accrued(basket_token) diff --git a/mango/__init__.py b/mango/__init__.py index bfc1f7a..54b9d89 100644 --- a/mango/__init__.py +++ b/mango/__init__.py @@ -20,7 +20,7 @@ from .group import GroupBasketMarket, Group from .idsjsontokenlookup import IdsJsonTokenLookup from .idsjsonmarketlookup import IdsJsonMarketLookup from .inventory import Inventory, InventoryAccountWatcher, spl_token_inventory_loader, account_inventory_loader -from .instructions import build_create_solana_account_instructions, build_create_spl_account_instructions, build_create_associated_spl_account_instructions, build_transfer_spl_tokens_instructions, build_close_spl_account_instructions, build_create_serum_open_orders_instructions, build_serum_place_order_instructions, build_serum_consume_events_instructions, build_serum_settle_instructions, build_spot_place_order_instructions, build_cancel_spot_order_instructions, build_cancel_perp_order_instructions, build_mango_consume_events_instructions, build_create_account_instructions, build_place_perp_order_instructions, build_deposit_instructions, build_withdraw_instructions +from .instructions import build_create_solana_account_instructions, build_create_spl_account_instructions, build_create_associated_spl_account_instructions, build_transfer_spl_tokens_instructions, build_close_spl_account_instructions, build_create_serum_open_orders_instructions, build_serum_place_order_instructions, build_serum_consume_events_instructions, build_serum_settle_instructions, build_spot_place_order_instructions, build_cancel_spot_order_instructions, build_cancel_perp_order_instructions, build_mango_consume_events_instructions, build_create_account_instructions, build_place_perp_order_instructions, build_deposit_instructions, build_withdraw_instructions, build_redeem_accrued_mango_instructions from .instructiontype import InstructionType from .liquidatablereport import LiquidatableState, LiquidatableReport from .liquidationevent import LiquidationEvent diff --git a/mango/client.py b/mango/client.py index 3a5a19c..106c3fb 100644 --- a/mango/client.py +++ b/mango/client.py @@ -13,6 +13,7 @@ # [Github](https://github.com/blockworks-foundation) # [Email](mailto:hello@blockworks.foundation) +import datetime import itertools import json import logging @@ -434,16 +435,20 @@ class BetterClient: def wait_for_confirmation(self, transaction_ids: typing.Sequence[str], max_wait_in_seconds: int = 60) -> typing.Sequence[str]: self.logger.info(f"Waiting up to {max_wait_in_seconds} seconds for {transaction_ids}.") all_confirmed: typing.List[str] = [] - for wait in range(0, max_wait_in_seconds): - for transaction_id in transaction_ids: + start_time: datetime.datetime = datetime.datetime.now() + cutoff: datetime.datetime = start_time + datetime.timedelta(seconds=max_wait_in_seconds) + for transaction_id in transaction_ids: + while datetime.datetime.now() < cutoff: time.sleep(1) confirmed = self.get_confirmed_transaction(transaction_id) if confirmed is not None: - self.logger.info(f"Confirmed {transaction_id} after {wait} seconds.") + self.logger.info( + f"Confirmed {transaction_id} after {datetime.datetime.now() - start_time} seconds.") all_confirmed += [transaction_id] + break if len(all_confirmed) != len(transaction_ids): - self.logger.info(f"Timed out after {wait} seconds waiting on transaction {transaction_id}.") + self.logger.info(f"Timed out after {max_wait_in_seconds} seconds waiting on transaction {transaction_id}.") return all_confirmed def __str__(self) -> str: diff --git a/mango/group.py b/mango/group.py index 124feb8..4abde8a 100644 --- a/mango/group.py +++ b/mango/group.py @@ -213,6 +213,13 @@ class Group(AddressableAccount): raise Exception(f"Could not find token info for mint {token.mint} in group {self.address}") + def find_token_info_by_symbol(self, symbol: str) -> TokenInfo: + for token_info in self.tokens: + if token_info is not None and token_info.token.symbol_matches(symbol): + return token_info + + raise Exception(f"Could not find token info for symbol '{symbol}' in group {self.address}") + def fetch_balances(self, context: Context, root_address: PublicKey) -> typing.Sequence[TokenValue]: balances: typing.List[TokenValue] = [] sol_balance = context.client.get_balance(root_address) diff --git a/mango/instructions.py b/mango/instructions.py index b058119..6b11a0c 100644 --- a/mango/instructions.py +++ b/mango/instructions.py @@ -20,7 +20,7 @@ import typing from decimal import Decimal from pyserum.enums import OrderType as SerumOrderType, Side as SerumSide from pyserum.instructions import settle_funds, SettleFundsParams -from pyserum.market import Market +from pyserum.market import Market as PySerumMarket from pyserum.open_orders_account import make_create_account_instruction from solana.account import Account as SolanaAccount from solana.publickey import PublicKey @@ -38,10 +38,12 @@ from .context import Context from .group import Group from .layouts import layouts from .orders import Order, OrderType, Side +from .perpmarket import PerpMarket from .perpmarketdetails import PerpMarketDetails from .rootbank import NodeBank, RootBank from .token import Token from .tokenaccount import TokenAccount +from .tokeninfo import TokenInfo from .wallet import Wallet @@ -127,7 +129,7 @@ def build_close_spl_account_instructions(context: Context, wallet: Wallet, addre # Creates a Serum openorders-creating instruction. # -def build_create_serum_open_orders_instructions(context: Context, wallet: Wallet, market: Market) -> CombinableInstructions: +def build_create_serum_open_orders_instructions(context: Context, wallet: Wallet, market: PySerumMarket) -> CombinableInstructions: new_open_orders_account = SolanaAccount() minimum_balance = context.client.get_minimum_balance_for_rent_exemption(layouts.OPEN_ORDERS.sizeof()) instruction = make_create_account_instruction( @@ -145,7 +147,7 @@ def build_create_serum_open_orders_instructions(context: Context, wallet: Wallet # Creates a Serum order-placing instruction using V3 of the NewOrder instruction. # -def build_serum_place_order_instructions(context: Context, wallet: Wallet, market: Market, source: PublicKey, open_orders_address: PublicKey, order_type: OrderType, side: Side, price: Decimal, quantity: Decimal, client_id: int, fee_discount_address: typing.Optional[PublicKey]) -> CombinableInstructions: +def build_serum_place_order_instructions(context: Context, wallet: Wallet, market: PySerumMarket, source: PublicKey, open_orders_address: PublicKey, order_type: OrderType, side: Side, price: Decimal, quantity: Decimal, client_id: int, fee_discount_address: typing.Optional[PublicKey]) -> CombinableInstructions: serum_order_type: SerumOrderType = SerumOrderType.POST_ONLY if order_type == OrderType.POST_ONLY else SerumOrderType.IOC if order_type == OrderType.IOC else SerumOrderType.LIMIT serum_side: SerumSide = SerumSide.SELL if side == Side.SELL else SerumSide.BUY @@ -194,7 +196,7 @@ def build_serum_consume_events_instructions(context: Context, market_address: Pu # Creates a 'settle' instruction. # -def build_serum_settle_instructions(context: Context, wallet: Wallet, market: Market, open_orders_address: PublicKey, base_token_account_address: PublicKey, quote_token_account_address: PublicKey) -> CombinableInstructions: +def build_serum_settle_instructions(context: Context, wallet: Wallet, market: PySerumMarket, open_orders_address: PublicKey, base_token_account_address: PublicKey, quote_token_account_address: PublicKey) -> CombinableInstructions: vault_signer = PublicKey.create_program_address( [bytes(market.state.public_key()), market.state.vault_signer_nonce().to_bytes(8, byteorder="little")], market.state.program_id(), @@ -239,12 +241,12 @@ def build_serum_settle_instructions(context: Context, wallet: Wallet, market: Ma # /// 13. `[writable]` quote_node_bank_ai - MangoGroup quote vault acc # /// 14. `[writable]` base_vault_ai - MangoGroup base vault acc # /// 15. `[writable]` quote_vault_ai - MangoGroup quote vault acc -# /// 16. `[]` dex_signer_ai - dex Market signer account +# /// 16. `[]` dex_signer_ai - dex PySerumMarket signer account # /// 17. `[]` spl token program def build_spot_settle_instructions(context: Context, wallet: Wallet, account: Account, - market: Market, group: Group, open_orders_address: PublicKey, + market: PySerumMarket, group: Group, open_orders_address: PublicKey, base_rootbank: RootBank, base_nodebank: NodeBank, quote_rootbank: RootBank, quote_nodebank: NodeBank) -> CombinableInstructions: vault_signer = PublicKey.create_program_address( @@ -300,7 +302,7 @@ def build_spot_settle_instructions(context: Context, wallet: Wallet, account: Ac # orderbook). # -def build_compound_serum_place_order_instructions(context: Context, wallet: Wallet, market: Market, source: PublicKey, open_orders_address: PublicKey, all_open_orders_addresses: typing.Sequence[PublicKey], order_type: OrderType, side: Side, price: Decimal, quantity: Decimal, client_id: int, base_token_account_address: PublicKey, quote_token_account_address: PublicKey, fee_discount_address: typing.Optional[PublicKey], consume_limit: int = 32) -> CombinableInstructions: +def build_compound_serum_place_order_instructions(context: Context, wallet: Wallet, market: PySerumMarket, source: PublicKey, open_orders_address: PublicKey, all_open_orders_addresses: typing.Sequence[PublicKey], order_type: OrderType, side: Side, price: Decimal, quantity: Decimal, client_id: int, base_token_account_address: PublicKey, quote_token_account_address: PublicKey, fee_discount_address: typing.Optional[PublicKey], consume_limit: int = 32) -> CombinableInstructions: place_order = build_serum_place_order_instructions( context, wallet, market, source, open_orders_address, order_type, side, price, quantity, client_id, fee_discount_address) consume_events = build_serum_consume_events_instructions( @@ -553,7 +555,7 @@ def build_withdraw_instructions(context: Context, wallet: Wallet, group: Group, return CombinableInstructions(signers=[], instructions=[withdraw]) -def build_spot_openorders_instructions(context: Context, wallet: Wallet, group: Group, account: Account, market: Market) -> CombinableInstructions: +def build_spot_openorders_instructions(context: Context, wallet: Wallet, group: Group, account: Account, market: PySerumMarket) -> CombinableInstructions: instructions: CombinableInstructions = CombinableInstructions.empty() create_open_orders = build_create_solana_account_instructions( context, wallet, context.dex_program_id, layouts.OPEN_ORDERS.sizeof()) @@ -611,7 +613,7 @@ def build_spot_openorders_instructions(context: Context, wallet: Wallet, group: # })), def build_spot_place_order_instructions(context: Context, wallet: Wallet, group: Group, account: Account, - market: Market, + market: PySerumMarket, order_type: OrderType, side: Side, price: Decimal, quantity: Decimal, client_id: int, fee_discount_address: typing.Optional[PublicKey]) -> CombinableInstructions: @@ -701,7 +703,7 @@ def build_spot_place_order_instructions(context: Context, wallet: Wallet, group: # -def build_cancel_spot_order_instructions(context: Context, wallet: Wallet, group: Group, account: Account, market: Market, order: Order, open_orders_address: PublicKey) -> CombinableInstructions: +def build_cancel_spot_order_instructions(context: Context, wallet: Wallet, group: Group, account: Account, market: PySerumMarket, order: Order, open_orders_address: PublicKey) -> CombinableInstructions: # { buy: 0, sell: 1 } raw_side: int = 1 if order.side == Side.SELL else 0 @@ -741,13 +743,12 @@ def build_cancel_spot_order_instructions(context: Context, wallet: Wallet, group ] return CombinableInstructions(signers=[], instructions=instructions) + # # 🥭 build_mango_settle_instructions function # # Creates a 'settle' instruction for Mango accounts. # - - -def build_mango_settle_instructions(context: Context, wallet: Wallet, market: Market, open_orders_address: PublicKey, base_token_account_address: PublicKey, quote_token_account_address: PublicKey) -> CombinableInstructions: +def build_mango_settle_instructions(context: Context, wallet: Wallet, market: PySerumMarket, open_orders_address: PublicKey, base_token_account_address: PublicKey, quote_token_account_address: PublicKey) -> CombinableInstructions: vault_signer = PublicKey.create_program_address( [bytes(market.state.public_key()), market.state.vault_signer_nonce().to_bytes(8, byteorder="little")], market.state.program_id(), @@ -767,3 +768,43 @@ def build_mango_settle_instructions(context: Context, wallet: Wallet, market: Ma ) return CombinableInstructions(signers=[], instructions=[instruction]) + + +# # 🥭 build_redeem_accrued_mango_instructions function +# +# Creates a 'RedeemMngo' instruction for Mango accounts. +# +def build_redeem_accrued_mango_instructions(context: Context, wallet: Wallet, perp_market: PerpMarket, group: Group, account: Account, mngo: TokenInfo) -> CombinableInstructions: + node_bank: NodeBank = mngo.root_bank.pick_node_bank(context) + # /// Redeem the mngo_accrued in a PerpAccount for MNGO in MangoAccount deposits + # /// + # /// Accounts expected by this instruction (11): + # /// 0. `[]` mango_group_ai - MangoGroup that this mango account is for + # /// 1. `[]` mango_cache_ai - MangoCache + # /// 2. `[writable]` mango_account_ai - MangoAccount + # /// 3. `[signer]` owner_ai - MangoAccount owner + # /// 4. `[]` perp_market_ai - PerpMarket + # /// 5. `[writable]` mngo_perp_vault_ai + # /// 6. `[]` mngo_root_bank_ai + # /// 7. `[writable]` mngo_node_bank_ai + # /// 8. `[writable]` mngo_bank_vault_ai + # /// 9. `[]` signer_ai - Group Signer Account + # /// 10. `[]` token_prog_ai - SPL Token program id + redeem_accrued_mango_instruction = TransactionInstruction( + keys=[ + AccountMeta(is_signer=False, is_writable=False, pubkey=group.address), + AccountMeta(is_signer=False, is_writable=False, pubkey=group.cache), + AccountMeta(is_signer=False, is_writable=True, pubkey=account.address), + AccountMeta(is_signer=True, is_writable=False, pubkey=wallet.address), + AccountMeta(is_signer=False, is_writable=False, pubkey=perp_market.address), + AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market.underlying_perp_market.mngo_vault), + AccountMeta(is_signer=False, is_writable=False, pubkey=mngo.root_bank.address), + AccountMeta(is_signer=False, is_writable=True, pubkey=node_bank.address), + AccountMeta(is_signer=False, is_writable=True, pubkey=node_bank.vault), + AccountMeta(is_signer=False, is_writable=False, pubkey=group.signer_key), + AccountMeta(is_signer=False, is_writable=False, pubkey=TOKEN_PROGRAM_ID) + ], + program_id=context.program_id, + data=layouts.REDEEM_MNGO.build(dict()) + ) + return CombinableInstructions(signers=[], instructions=[redeem_accrued_mango_instruction]) diff --git a/mango/layouts/layouts.py b/mango/layouts/layouts.py index 190cd58..2c13f6f 100644 --- a/mango/layouts/layouts.py +++ b/mango/layouts/layouts.py @@ -1335,6 +1335,24 @@ INIT_SPOT_OPEN_ORDERS = construct.Struct( "variant" / construct.Const(32, construct.BytesInteger(4, swapped=True)) ) +# /// Redeem the mngo_accrued in a PerpAccount for MNGO in MangoAccount deposits +# /// +# /// Accounts expected by this instruction (11): +# /// 0. `[]` mango_group_ai - MangoGroup that this mango account is for +# /// 1. `[]` mango_cache_ai - MangoCache +# /// 2. `[writable]` mango_account_ai - MangoAccount +# /// 3. `[signer]` owner_ai - MangoAccount owner +# /// 4. `[]` perp_market_ai - PerpMarket +# /// 5. `[writable]` mngo_perp_vault_ai +# /// 6. `[]` mngo_root_bank_ai +# /// 7. `[writable]` mngo_node_bank_ai +# /// 8. `[writable]` mngo_bank_vault_ai +# /// 9. `[]` signer_ai - Group Signer Account +# /// 10. `[]` token_prog_ai - SPL Token program id +REDEEM_MNGO = construct.Struct( + "variant" / construct.Const(33, construct.BytesInteger(4, swapped=True)) +) + UNSPECIFIED = construct.Struct( "variant" / DecimalAdapter(4) ) @@ -1373,7 +1391,7 @@ InstructionParsersByVariant = { 30: UNSPECIFIED, # RESOLVE_PERP_BANKRUPTCY, 31: UNSPECIFIED, # RESOLVE_TOKEN_BANKRUPTCY, 32: INIT_SPOT_OPEN_ORDERS, # INIT_SPOT_OPEN_ORDERS, - 33: UNSPECIFIED, # REDEEM_MNGO, + 33: REDEEM_MNGO, # REDEEM_MNGO, 34: UNSPECIFIED, # ADD_MANGO_ACCOUNT_INFO, 35: UNSPECIFIED, # DEPOSIT_MSRM, 36: UNSPECIFIED, # WITHDRAW_MSRM