# # ⚠ 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 pyserum.enums import typing from decimal import Decimal from pyserum.enums import OrderType as SerumOrderType, Side as SerumSide from pyserum.instructions import ConsumeEventsParams, consume_events, settle_funds, SettleFundsParams from pyserum.market import Market from pyserum.open_orders_account import make_create_account_instruction from solana.account import Account as SolanaAccount from solana.publickey import PublicKey from solana.system_program import CreateAccountParams, create_account from solana.sysvar import SYSVAR_CLOCK_PUBKEY, SYSVAR_RENT_PUBKEY from solana.transaction import AccountMeta, TransactionInstruction from spl.token.constants import ACCOUNT_LEN, TOKEN_PROGRAM_ID from spl.token.instructions import CloseAccountParams, InitializeAccountParams, Transfer2Params, close_account, initialize_account, transfer2 from .account import Account from .combinableinstructions import CombinableInstructions from .constants import SYSTEM_PROGRAM_ADDRESS from .context import Context from .group import Group from .layouts import layouts from .orders import Order, OrderType, Side from .perpmarket import PerpMarket from .rootbank import NodeBank, RootBank from .token import Token from .tokenaccount import TokenAccount from .wallet import Wallet # 🥭 Instructions # # This file contains the low-level instruction functions that build the raw instructions # to send to Solana. # # One important distinction between these functions and the more common `create instruction functions` in # Solana is that these functions *all return a list of instructions and signers*. # # It's likely that some operations will require actions split across multiple instructions because of # instruction size limitiations, so all our functions are prepared for this without having to change # the function signature in future. # # # 🥭 _ensure_openorders function # # Unlike most functions in this file, `_ensure_openorders()` returns a tuple, not just an `CombinableInstructions`. # # The idea is: callers just want to know the OpenOrders address, but if it doesn't exist they may need to add # the instructions and signers for its creation before they try to use it. # # This function will always return the proper OpenOrders address, and will also return the `CombinableInstructions` to # create it (which will be empty of signers and instructions if the OpenOrders already exists). # def _ensure_openorders(context: Context, wallet: Wallet, group: Group, account: Account, market: Market) -> typing.Tuple[PublicKey, CombinableInstructions]: spot_market_address = market.state.public_key() market_index = group.find_spot_market_index(spot_market_address) open_orders_address = account.spot_open_orders[market_index] if open_orders_address is not None: return open_orders_address, CombinableInstructions.empty() creation = build_create_solana_account_instructions( context, wallet, context.dex_program_id, layouts.OPEN_ORDERS.sizeof()) open_orders_address = creation.signers[0].public_key() # This is maybe a little nasty - updating the existing structure with the new OO account. account.spot_open_orders[market_index] = open_orders_address return open_orders_address, creation # # 🥭 build_create_solana_account_instructions function # # Creates and initializes an SPL token account. Can add additional lamports too but that's usually not # necesary. # def build_create_solana_account_instructions(context: Context, wallet: Wallet, program_id: PublicKey, size: int, lamports: int = 0) -> CombinableInstructions: minimum_balance_response = context.client.get_minimum_balance_for_rent_exemption( size, commitment=context.commitment) minimum_balance = context.unwrap_or_raise_exception(minimum_balance_response) account = SolanaAccount() create_instruction = create_account( CreateAccountParams(wallet.address, account.public_key(), lamports + minimum_balance, size, program_id)) return CombinableInstructions(signers=[account], instructions=[create_instruction]) # # 🥭 build_create_spl_account_instructions function # # Creates and initializes an SPL token account. Can add additional lamports too but that's usually not # necesary. # def build_create_spl_account_instructions(context: Context, wallet: Wallet, token: Token, address: PublicKey, lamports: int = 0) -> CombinableInstructions: create_instructions = build_create_solana_account_instructions(context, wallet, TOKEN_PROGRAM_ID, ACCOUNT_LEN, lamports) initialize_instruction = initialize_account(InitializeAccountParams( TOKEN_PROGRAM_ID, address, token.mint, wallet.address)) return create_instructions + CombinableInstructions(signers=[], instructions=[initialize_instruction]) # # 🥭 build_transfer_spl_tokens_instructions function # # Creates an instruction to transfer SPL tokens from one account to another. # def build_transfer_spl_tokens_instructions(context: Context, wallet: Wallet, token: Token, source: PublicKey, destination: PublicKey, quantity: Decimal) -> CombinableInstructions: amount = int(quantity * (10 ** token.decimals)) instructions = [transfer2(Transfer2Params(TOKEN_PROGRAM_ID, source, token.mint, destination, wallet.address, amount, int(token.decimals)))] return CombinableInstructions(signers=[], instructions=instructions) # # 🥭 build_close_spl_account_instructions function # # Creates an instructio to close an SPL token account and transfers any remaining lamports to the wallet. # def build_close_spl_account_instructions(context: Context, wallet: Wallet, address: PublicKey) -> CombinableInstructions: return CombinableInstructions(signers=[], instructions=[close_account(CloseAccountParams(TOKEN_PROGRAM_ID, address, wallet.address, wallet.address))]) # # 🥭 build_create_serum_open_orders_instructions function # # Creates a Serum openorders-creating instruction. # def build_create_serum_open_orders_instructions(context: Context, wallet: Wallet, market: Market) -> CombinableInstructions: new_open_orders_account = SolanaAccount() response = context.client.get_minimum_balance_for_rent_exemption( layouts.OPEN_ORDERS.sizeof(), commitment=context.commitment) balanced_needed = context.unwrap_or_raise_exception(response) instruction = make_create_account_instruction( owner_address=wallet.address, new_account_address=new_open_orders_account.public_key(), lamports=balanced_needed, program_id=market.state.program_id(), ) return CombinableInstructions(signers=[new_open_orders_account], instructions=[instruction]) # # 🥭 build_serum_place_order_instructions function # # 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: 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 instruction = market.make_place_order_instruction( source, wallet.account, serum_order_type, serum_side, float(price), float(quantity), client_id, open_orders_address, fee_discount_address ) return CombinableInstructions(signers=[], instructions=[instruction]) # # 🥭 build_serum_consume_events_instructions function # # Creates an event-consuming 'crank' instruction. # def build_serum_consume_events_instructions(context: Context, wallet: Wallet, market: Market, open_orders_addresses: typing.Sequence[PublicKey], limit: int = 32) -> CombinableInstructions: instruction = consume_events(ConsumeEventsParams( market=market.state.public_key(), event_queue=market.state.event_queue(), open_orders_accounts=open_orders_addresses, program_id=context.dex_program_id, limit=limit )) # The interface accepts (and currently requires) two accounts at the end, but # it doesn't actually use them. random_account = SolanaAccount().public_key() instruction.keys.append(AccountMeta(random_account, is_signer=False, is_writable=False)) instruction.keys.append(AccountMeta(random_account, is_signer=False, is_writable=False)) return CombinableInstructions(signers=[], instructions=[instruction]) # # 🥭 build_serum_settle_instructions function # # 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: 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(), ) instruction = settle_funds( SettleFundsParams( market=market.state.public_key(), open_orders=open_orders_address, owner=wallet.address, base_vault=market.state.base_vault(), quote_vault=market.state.quote_vault(), base_wallet=base_token_account_address, quote_wallet=quote_token_account_address, vault_signer=vault_signer, program_id=market.state.program_id(), ) ) return CombinableInstructions(signers=[], instructions=[instruction]) # # 🥭 build_compound_serum_place_order_instructions function # # This function puts a trade on the Serum orderbook and then cranks and settles. # It follows the pattern described here: # https://solanadev.blogspot.com/2021/05/order-techniques-with-project-serum.html # # Here's an example (Raydium?) transaction that does this: # https://solanabeach.io/transaction/3Hb2h7QMM3BbJCK42BUDuVEYwwaiqfp2oQUZMDJvUuoyCRJD5oBmA3B8oAGkB9McdCFtwdT2VrSKM2GCKhJ92FpY # # Basically, it tries to send to a 'buy/sell' and settle all in one transaction. # # It does this by: # * Sending a Place Order (V3) instruction # * Sending a Consume Events (crank) instruction # * Sending a Settle Funds instruction # all in the same transaction. With V3 Serum, this should consistently settle funds to the wallet # immediately if the order is filled (either because it's IOC or because it matches an order on the # 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: 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( context, wallet, market, all_open_orders_addresses, consume_limit) settle = build_serum_settle_instructions( context, wallet, market, open_orders_address, base_token_account_address, quote_token_account_address) return place_order + consume_events + settle # # 🥭 build_cancel_perp_order_instruction function # # Builds the instructions necessary for cancelling a perp order. # def build_cancel_perp_order_instructions(context: Context, wallet: Wallet, account: Account, perp_market: PerpMarket, order: Order) -> CombinableInstructions: # Prefer cancelling by client ID so we don't have to keep track of the order side. if order.client_id != 0: data: bytes = layouts.CANCEL_PERP_ORDER_BY_CLIENT_ID.build( { "client_order_id": order.client_id }) else: # { buy: 0, sell: 1 } raw_side: int = 1 if order.side == Side.SELL else 0 data = layouts.CANCEL_PERP_ORDER.build( { "order_id": order.id, "side": raw_side }) # Accounts expected by this instruction (both CANCEL_PERP_ORDER and CANCEL_PERP_ORDER_BY_CLIENT_ID are the same): # { isSigner: false, isWritable: false, pubkey: mangoGroupPk }, # { isSigner: false, isWritable: true, pubkey: mangoAccountPk }, # { isSigner: true, isWritable: false, pubkey: ownerPk }, # { isSigner: false, isWritable: false, pubkey: perpMarketPk }, # { isSigner: false, isWritable: true, pubkey: bidsPk }, # { isSigner: false, isWritable: true, pubkey: asksPk }, instructions = [ TransactionInstruction( keys=[ AccountMeta(is_signer=False, is_writable=False, pubkey=account.group.address), 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.bids), AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market.asks) ], program_id=context.program_id, data=data ) ] return CombinableInstructions(signers=[], instructions=instructions) def build_place_perp_order_instructions(context: Context, wallet: Wallet, group: Group, account: Account, perp_market: PerpMarket, price: Decimal, quantity: Decimal, client_order_id: int, side: Side, order_type: OrderType) -> CombinableInstructions: # { buy: 0, sell: 1 } raw_side: int = 1 if side == Side.SELL else 0 # { limit: 0, ioc: 1, postOnly: 2 } raw_order_type: int = 2 if order_type == OrderType.POST_ONLY else 1 if order_type == OrderType.IOC else 0 base_decimals = perp_market.base_token.decimals quote_decimals = perp_market.quote_token.decimals base_factor = Decimal(10) ** base_decimals quote_factor = Decimal(10) ** quote_decimals native_price = ((price * quote_factor) * perp_market.base_lot_size) / (perp_market.quote_lot_size * base_factor) native_quantity = (quantity * base_factor) / perp_market.base_lot_size # /// Accounts expected by this instruction (6): # /// 0. `[]` mango_group_ai - TODO # /// 1. `[writable]` mango_account_ai - TODO # /// 2. `[signer]` owner_ai - TODO # /// 3. `[]` mango_cache_ai - TODO # /// 4. `[writable]` perp_market_ai - TODO # /// 5. `[writable]` bids_ai - TODO # /// 6. `[writable]` asks_ai - TODO # /// 7. `[writable]` event_queue_ai - TODO instructions = [ TransactionInstruction( keys=[ AccountMeta(is_signer=False, is_writable=False, pubkey=group.address), 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=group.cache), AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market.address), AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market.bids), AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market.asks), AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market.event_queue), *list([AccountMeta(is_signer=False, is_writable=False, pubkey=oo_address or SYSTEM_PROGRAM_ADDRESS) for oo_address in account.spot_open_orders]) ], program_id=context.program_id, data=layouts.PLACE_PERP_ORDER.build( { "price": native_price, "quantity": native_quantity, "client_order_id": client_order_id, "side": raw_side, "order_type": raw_order_type }) ) ] return CombinableInstructions(signers=[], instructions=instructions) def build_mango_consume_events_instructions(context: Context, wallet: Wallet, group: Group, account: Account, perp_market: PerpMarket, limit: Decimal = Decimal(32)) -> CombinableInstructions: # Accounts expected by this instruction (6): # 0. `[]` mangoGroupPk # 1. `[]` perpMarketPk # 2. `[writable]` eventQueuePk # 3+ `[writable]` mangoAccountPks... instructions = [ TransactionInstruction( keys=[ AccountMeta(is_signer=False, is_writable=False, pubkey=group.address), AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market.address), AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market.event_queue), AccountMeta(is_signer=False, is_writable=True, pubkey=account.address) ], program_id=context.program_id, data=layouts.CONSUME_EVENTS.build( { "limit": limit, }) ) ] return CombinableInstructions(signers=[], instructions=instructions) def build_create_account_instructions(context: Context, wallet: Wallet, group: Group) -> CombinableInstructions: create_account_instructions = build_create_solana_account_instructions( context, wallet, context.program_id, layouts.MANGO_ACCOUNT) mango_account_address = create_account_instructions.signers[0].public_key() # /// 0. `[]` mango_group_ai - Group that this mango account is for # /// 1. `[writable]` mango_account_ai - the mango account data # /// 2. `[signer]` owner_ai - Solana account of owner of the mango account # /// 3. `[]` rent_ai - Rent sysvar account init = TransactionInstruction( keys=[ AccountMeta(is_signer=False, is_writable=False, pubkey=group.address), AccountMeta(is_signer=False, is_writable=True, pubkey=mango_account_address), AccountMeta(is_signer=True, is_writable=False, pubkey=wallet.address), AccountMeta(is_signer=False, is_writable=False, pubkey=SYSVAR_CLOCK_PUBKEY) ], program_id=context.program_id, data=layouts.INIT_MANGO_ACCOUNT.build({}) ) return create_account_instructions + CombinableInstructions(signers=[], instructions=[init]) # /// Withdraw funds that were deposited earlier. # /// # /// Accounts expected by this instruction (10): # /// # /// 0. `[read]` mango_group_ai, - # /// 1. `[write]` mango_account_ai, - # /// 2. `[read]` owner_ai, - # /// 3. `[read]` mango_cache_ai, - # /// 4. `[read]` root_bank_ai, - # /// 5. `[write]` node_bank_ai, - # /// 6. `[write]` vault_ai, - # /// 7. `[write]` token_account_ai, - # /// 8. `[read]` signer_ai, - # /// 9. `[read]` token_prog_ai, - # /// 10. `[read]` clock_ai, - # /// 11..+ `[]` open_orders_accs - open orders for each of the spot market # Withdraw { # quantity: u64, # allow_borrow: bool, # }, def build_withdraw_instructions(context: Context, wallet: Wallet, group: Group, account: Account, root_bank: RootBank, node_bank: NodeBank, token_account: TokenAccount, allow_borrow: bool) -> CombinableInstructions: value = token_account.value.shift_to_native().value withdraw = TransactionInstruction( keys=[ AccountMeta(is_signer=False, is_writable=False, pubkey=group.address), 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=group.cache), AccountMeta(is_signer=False, is_writable=False, pubkey=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=True, pubkey=token_account.address), AccountMeta(is_signer=False, is_writable=False, pubkey=group.signer_key), AccountMeta(is_signer=False, is_writable=False, pubkey=TOKEN_PROGRAM_ID), *list([AccountMeta(is_signer=False, is_writable=False, pubkey=oo_address or SYSTEM_PROGRAM_ADDRESS) for oo_address in account.spot_open_orders]) ], program_id=context.program_id, data=layouts.WITHDRAW.build({ "quantity": value, "allow_borrow": allow_borrow }) ) return CombinableInstructions(signers=[], instructions=[withdraw]) # # 🥭 build_mango_place_order_instructions function # # Creates a Mango order-placing instruction using the Serum instruction as the inner instruction. # def build_spot_place_order_instructions(context: Context, wallet: Wallet, group: Group, account: Account, market: Market, order_type: OrderType, side: Side, price: Decimal, quantity: Decimal, client_id: int, fee_discount_address: typing.Optional[PublicKey]) -> CombinableInstructions: instructions: CombinableInstructions = CombinableInstructions.empty() open_orders_address, create_open_orders = _ensure_openorders(context, wallet, group, account, market) instructions += create_open_orders serum_order_type = pyserum.enums.OrderType.POST_ONLY if order_type == OrderType.POST_ONLY else pyserum.enums.OrderType.IOC if order_type == OrderType.IOC else pyserum.enums.OrderType.LIMIT serum_side = pyserum.enums.Side.BUY if side == Side.BUY else pyserum.enums.Side.SELL intrinsic_price = market.state.price_number_to_lots(float(price)) max_base_quantity = market.state.base_size_number_to_lots(float(quantity)) max_quote_quantity = market.state.base_size_number_to_lots( float(quantity)) * market.state.quote_lot_size() * market.state.price_number_to_lots(float(price)) quote_token_info = group.shared_quote_token base_token_infos = [ token_info for token_info in group.base_tokens if token_info is not None and token_info.token.mint == market.state.base_mint()] if len(base_token_infos) != 1: raise Exception( f"Could not find base token info for group {group.address} - length was {len(base_token_infos)} when it should be 1.") base_token_info = base_token_infos[0] 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(), ) base_node_bank = base_token_info.root_bank.pick_node_bank(context) quote_node_bank = quote_token_info.root_bank.pick_node_bank(context) # /// Accounts expected by this instruction (22+openorders): # { isSigner: false, isWritable: false, pubkey: mangoGroupPk }, # { isSigner: false, isWritable: true, pubkey: mangoAccountPk }, # { isSigner: true, isWritable: false, pubkey: ownerPk }, # { isSigner: false, isWritable: false, pubkey: mangoCachePk }, # { isSigner: false, isWritable: false, pubkey: serumDexPk }, # { isSigner: false, isWritable: true, pubkey: spotMarketPk }, # { isSigner: false, isWritable: true, pubkey: bidsPk }, # { isSigner: false, isWritable: true, pubkey: asksPk }, # { isSigner: false, isWritable: true, pubkey: requestQueuePk }, # { isSigner: false, isWritable: true, pubkey: eventQueuePk }, # { isSigner: false, isWritable: true, pubkey: spotMktBaseVaultPk }, # { isSigner: false, isWritable: true, pubkey: spotMktQuoteVaultPk }, # { isSigner: false, isWritable: false, pubkey: baseRootBankPk }, # { isSigner: false, isWritable: true, pubkey: baseNodeBankPk }, # { isSigner: false, isWritable: true, pubkey: quoteRootBankPk }, # { isSigner: false, isWritable: true, pubkey: quoteNodeBankPk }, # { isSigner: false, isWritable: true, pubkey: quoteVaultPk }, # { isSigner: false, isWritable: true, pubkey: baseVaultPk }, # { isSigner: false, isWritable: false, pubkey: TOKEN_PROGRAM_ID }, # { isSigner: false, isWritable: false, pubkey: signerPk }, # { isSigner: false, isWritable: false, pubkey: SYSVAR_RENT_PUBKEY }, # { isSigner: false, isWritable: false, pubkey: dexSignerPk }, # { isSigner: false, isWritable: false, pubkey: msrmOrSrmVaultPk }, # ...openOrders.map(({ pubkey, isWritable }) => ({ # isSigner: false, # isWritable, # pubkey, # })), fee_discount_address_meta: typing.List[AccountMeta] = [] if fee_discount_address is not None: fee_discount_address_meta = [AccountMeta(is_signer=False, is_writable=False, pubkey=fee_discount_address)] place_spot_instruction = TransactionInstruction( keys=[ AccountMeta(is_signer=False, is_writable=False, pubkey=group.address), 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=group.cache), AccountMeta(is_signer=False, is_writable=False, pubkey=context.dex_program_id), AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.public_key()), AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.bids()), AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.asks()), AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.request_queue()), AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.event_queue()), AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.base_vault()), AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.quote_vault()), AccountMeta(is_signer=False, is_writable=False, pubkey=base_token_info.root_bank.address), AccountMeta(is_signer=False, is_writable=True, pubkey=base_node_bank.address), AccountMeta(is_signer=False, is_writable=True, pubkey=quote_token_info.root_bank.address), AccountMeta(is_signer=False, is_writable=True, pubkey=quote_node_bank.address), AccountMeta(is_signer=False, is_writable=True, pubkey=quote_node_bank.vault), AccountMeta(is_signer=False, is_writable=True, pubkey=base_node_bank.vault), AccountMeta(is_signer=False, is_writable=False, pubkey=TOKEN_PROGRAM_ID), AccountMeta(is_signer=False, is_writable=False, pubkey=group.signer_key), AccountMeta(is_signer=False, is_writable=False, pubkey=SYSVAR_RENT_PUBKEY), AccountMeta(is_signer=False, is_writable=False, pubkey=vault_signer), AccountMeta(is_signer=False, is_writable=False, pubkey=group.srm_vault or SYSTEM_PROGRAM_ADDRESS), *list([AccountMeta(is_signer=False, is_writable=(oo_address == open_orders_address), pubkey=oo_address or SYSTEM_PROGRAM_ADDRESS) for oo_address in account.spot_open_orders]), *fee_discount_address_meta ], program_id=context.program_id, data=layouts.PLACE_SPOT_ORDER.build( dict( side=serum_side, limit_price=intrinsic_price, max_base_quantity=max_base_quantity, max_quote_quantity=max_quote_quantity, self_trade_behavior=pyserum.enums.SelfTradeBehavior.DECREMENT_TAKE, order_type=serum_order_type, client_id=client_id, limit=65535, ) ) ) return instructions + CombinableInstructions(signers=[], instructions=[place_spot_instruction]) def build_compound_spot_place_order_instructions(context: Context, wallet: Wallet, group: Group, account: Account, market: Market, source: PublicKey, order_type: OrderType, side: Side, price: Decimal, quantity: Decimal, client_id: int, fee_discount_address: typing.Optional[PublicKey]) -> CombinableInstructions: _, create_open_orders = _ensure_openorders(context, wallet, group, account, market) place_order = build_spot_place_order_instructions(context, wallet, group, account, market, order_type, side, price, quantity, client_id, fee_discount_address) open_orders_addresses = list([oo for oo in account.spot_open_orders if oo is not None]) consume_events = build_serum_consume_events_instructions(context, wallet, market, open_orders_addresses) # quote_token_info = group.shared_quote_token # base_token_infos = [ # token_info for token_info in group.base_tokens if token_info is not None and token_info.token.mint == market.state.base_mint()] # if len(base_token_infos) != 1: # raise Exception( # f"Could not find base token info for group {group.address} - length was {len(base_token_infos)} when it should be 1.") # base_token_info = base_token_infos[0] # base_token_account = TokenAccount.fetch_largest_for_owner_and_token(context, wallet.address, base_token_info.token) # quote_token_account = TokenAccount.fetch_largest_for_owner_and_token( # context, wallet.address, quote_token_info.token) # settle: CombinableInstructions = CombinableInstructions.empty() # if base_token_account is not None and quote_token_account is not None: # open_order_accounts = market.find_open_orders_accounts_for_owner(wallet.address) # settlement_open_orders = [oo for oo in open_order_accounts if oo.market == market.state.public_key()] # if len(settlement_open_orders) > 0 and settlement_open_orders[0] is not None: # settle = build_serum_settle_instructions( # context, wallet, market, open_orders_address, base_token_account.address, quote_token_account.address) combined = create_open_orders + place_order + consume_events # + settle return combined # # 🥭 build_cancel_spot_order_instruction function # # Builds the instructions necessary for cancelling a spot order. # def build_cancel_spot_order_instructions(context: Context, wallet: Wallet, group: Group, account: Account, market: Market, order: Order, open_orders_address: PublicKey) -> CombinableInstructions: # { buy: 0, sell: 1 } raw_side: int = 1 if order.side == Side.SELL else 0 # Accounts expected by this instruction: # { isSigner: false, isWritable: false, pubkey: mangoGroupPk }, # { isSigner: true, isWritable: false, pubkey: ownerPk }, # { isSigner: false, isWritable: false, pubkey: mangoAccountPk }, # { isSigner: false, isWritable: false, pubkey: dexProgramId }, # { isSigner: false, isWritable: true, pubkey: spotMarketPk }, # { isSigner: false, isWritable: true, pubkey: bidsPk }, # { isSigner: false, isWritable: true, pubkey: asksPk }, # { isSigner: false, isWritable: true, pubkey: openOrdersPk }, # { isSigner: false, isWritable: false, pubkey: signerKey }, # { isSigner: false, isWritable: true, pubkey: eventQueuePk }, instructions = [ TransactionInstruction( keys=[ AccountMeta(is_signer=False, is_writable=False, pubkey=group.address), AccountMeta(is_signer=True, is_writable=False, pubkey=wallet.address), AccountMeta(is_signer=False, is_writable=False, pubkey=account.address), AccountMeta(is_signer=False, is_writable=False, pubkey=context.dex_program_id), AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.public_key()), AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.bids()), AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.asks()), AccountMeta(is_signer=False, is_writable=True, pubkey=open_orders_address), AccountMeta(is_signer=False, is_writable=False, pubkey=group.signer_key), AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.event_queue()) ], program_id=context.program_id, data=layouts.CANCEL_SPOT_ORDER.build( { "order_id": order.id, "side": raw_side }) ) ] return CombinableInstructions(signers=[], instructions=instructions)