Added a redeem-mango command.

This commit is contained in:
Geoff Taylor 2021-08-11 19:14:26 +01:00
parent 7c6d1d43cf
commit 162f41e2d0
7 changed files with 204 additions and 20 deletions

View File

@ -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")

113
bin/redeem-mango Executable file
View File

@ -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)

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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])

View File

@ -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