diff --git a/bin/ensure-account b/bin/ensure-account new file mode 100755 index 0000000..8942ee9 --- /dev/null +++ b/bin/ensure-account @@ -0,0 +1,40 @@ +#!/usr/bin/env pyston3 + +import argparse +import logging +import os +import os.path +import sys + +sys.path.insert(0, os.path.abspath( + os.path.join(os.path.dirname(__file__), '..'))) +import mango # nopep8 + +parser = argparse.ArgumentParser(description="Ensure a Mango account exists for the wallet and group.") +mango.ContextBuilder.add_command_line_parameters(parser) +mango.Wallet.add_command_line_parameters(parser) +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) + +context = mango.ContextBuilder.from_command_line_parameters(args) +wallet = mango.Wallet.from_command_line_parameters_or_raise(args) + +group = mango.Group.load(context) +accounts = mango.Account.load_all_for_owner(context, wallet.address, group) + +if len(accounts) > 0: + print(f"At least one account already exists for group {group.address} and wallet {wallet.address}") +else: + signers: mango.CombinableInstructions = mango.CombinableInstructions.from_wallet(wallet) + init = mango.build_create_account_instructions(context, wallet, group) + all_instructions = signers + init + transaction_ids = all_instructions.execute(context) + + print("Created account.") + if args.wait: + print("Waiting on transaction IDs:", transaction_ids) + context.client.wait_for_confirmation(transaction_ids) diff --git a/bin/ensure-open-orders b/bin/ensure-open-orders new file mode 100755 index 0000000..ba6bba4 --- /dev/null +++ b/bin/ensure-open-orders @@ -0,0 +1,43 @@ +#!/usr/bin/env pyston3 + +import argparse +import logging +import os +import os.path +import sys + +sys.path.insert(0, os.path.abspath( + os.path.join(os.path.dirname(__file__), '..'))) +import mango # nopep8 + +parser = argparse.ArgumentParser(description="Ensure an OpenOrders account exists for the wallet and market.") +mango.ContextBuilder.add_command_line_parameters(parser) +mango.Wallet.add_command_line_parameters(parser) +parser.add_argument("--account-index", type=int, default=0, + help="index of the account to use, if more than one available") +parser.add_argument("--market", type=str, required=True, help="market symbol to buy (e.g. ETH/USDC)") +parser.add_argument("--dry-run", action="store_true", default=False, + help="runs as read-only and does not perform any transactions") +args = parser.parse_args() + +logging.getLogger().setLevel(args.log_level) +logging.warning(mango.WARNING_DISCLAIMER_TEXT) + +context = mango.ContextBuilder.from_command_line_parameters(args) +wallet = mango.Wallet.from_command_line_parameters_or_raise(args) + +group = mango.Group.load(context) +accounts = mango.Account.load_all_for_owner(context, wallet.address, group) +if len(accounts) == 0: + raise Exception(f"No Mango account exists for group {group.address} and wallet {wallet.address}") +account = accounts[args.account_index] + +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}") + +loaded_market: mango.Market = mango.ensure_market_loaded(context, market) +market_operations = mango.create_market_operations(context, wallet, account, market, args.dry_run) +open_orders = market_operations.ensure_openorders() +print(f"OpenOrders account for {market.symbol} is {open_orders}") diff --git a/bin/redeem-mango b/bin/redeem-mango index 116ee7f..0862e04 100755 --- a/bin/redeem-mango +++ b/bin/redeem-mango @@ -20,7 +20,8 @@ def report_accrued(basket_token: mango.AccountBasketBaseToken): 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, + perp_market = mango.PerpMarket(context.program_id, 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 diff --git a/bin/show-accounts b/bin/show-accounts index 54b0a2d..f3ab250 100755 --- a/bin/show-accounts +++ b/bin/show-accounts @@ -16,7 +16,7 @@ parser = argparse.ArgumentParser(description="Shows details of a Merps account." mango.ContextBuilder.add_command_line_parameters(parser) mango.Wallet.add_command_line_parameters(parser) parser.add_argument("--address", type=PublicKey, required=False, - help="address of Merps account (defaults to the root address of the wallet)") + help="address of account (defaults to the root address of the wallet)") args = parser.parse_args() logging.getLogger().setLevel(args.log_level) diff --git a/mango/contextbuilder.py b/mango/contextbuilder.py index 0c1a78e..e6111ab 100644 --- a/mango/contextbuilder.py +++ b/mango/contextbuilder.py @@ -151,11 +151,12 @@ class ContextBuilder: ids_json_market_lookup: MarketLookup = IdsJsonMarketLookup(cluster) all_market_lookup = ids_json_market_lookup if cluster == "mainnet-beta": - mainnet_serum_market_lookup: SerumMarketLookup = SerumMarketLookup.load(token_filename) + mainnet_serum_market_lookup: SerumMarketLookup = SerumMarketLookup.load(dex_program_id, token_filename) all_market_lookup = CompoundMarketLookup([ids_json_market_lookup, mainnet_serum_market_lookup]) elif cluster == "devnet": devnet_token_filename = token_filename.rsplit('.', 1)[0] + ".devnet.json" - devnet_serum_market_lookup: SerumMarketLookup = SerumMarketLookup.load(devnet_token_filename) + devnet_serum_market_lookup: SerumMarketLookup = SerumMarketLookup.load( + dex_program_id, devnet_token_filename) all_market_lookup = CompoundMarketLookup([ids_json_market_lookup, devnet_serum_market_lookup]) market_lookup: MarketLookup = all_market_lookup diff --git a/mango/idsjsonmarketlookup.py b/mango/idsjsonmarketlookup.py index f055761..499edc9 100644 --- a/mango/idsjsonmarketlookup.py +++ b/mango/idsjsonmarketlookup.py @@ -50,15 +50,15 @@ class IdsJsonMarketLookup(MarketLookup): self.cluster: str = cluster @staticmethod - def _from_dict(market_type: IdsJsonMarketType, group_address: PublicKey, data: typing.Dict, tokens: typing.Sequence[Token], quote_symbol: str) -> Market: + def _from_dict(market_type: IdsJsonMarketType, program_id: PublicKey, group_address: PublicKey, data: typing.Dict, tokens: typing.Sequence[Token], quote_symbol: str) -> Market: base_symbol = data["baseSymbol"] base = Token.find_by_symbol(tokens, base_symbol) quote = Token.find_by_symbol(tokens, quote_symbol) address = PublicKey(data["publicKey"]) if market_type == IdsJsonMarketType.PERP: - return PerpMarketStub(address, base, quote, group_address) + return PerpMarketStub(program_id, address, base, quote, group_address) else: - return SpotMarketStub(address, base, quote, group_address) + return SpotMarketStub(program_id, address, base, quote, group_address) @staticmethod def _load_tokens(data: typing.Dict) -> typing.Sequence[Token]: @@ -82,30 +82,32 @@ class IdsJsonMarketLookup(MarketLookup): for group in MangoConstants["groups"]: if group["cluster"] == self.cluster: group_address: PublicKey = PublicKey(group["publicKey"]) + program_id: PublicKey = PublicKey(group["mangoProgramId"]) if check_perps: for market_data in group["perpMarkets"]: if market_data["name"].upper() == symbol.upper(): tokens = IdsJsonMarketLookup._load_tokens(group["tokens"]) - return IdsJsonMarketLookup._from_dict(IdsJsonMarketType.PERP, group_address, market_data, tokens, group["quoteSymbol"]) + return IdsJsonMarketLookup._from_dict(IdsJsonMarketType.PERP, program_id, group_address, market_data, tokens, group["quoteSymbol"]) if check_spots: for market_data in group["spotMarkets"]: if market_data["name"].upper() == symbol.upper(): tokens = IdsJsonMarketLookup._load_tokens(group["tokens"]) - return IdsJsonMarketLookup._from_dict(IdsJsonMarketType.SPOT, group_address, market_data, tokens, group["quoteSymbol"]) + return IdsJsonMarketLookup._from_dict(IdsJsonMarketType.SPOT, program_id, group_address, market_data, tokens, group["quoteSymbol"]) return None def find_by_address(self, address: PublicKey) -> typing.Optional[Market]: for group in MangoConstants["groups"]: if group["cluster"] == self.cluster: group_address: PublicKey = PublicKey(group["publicKey"]) + program_id: PublicKey = PublicKey(group["mangoProgramId"]) for market_data in group["perpMarkets"]: if market_data["key"] == str(address): tokens = IdsJsonMarketLookup._load_tokens(group["tokens"]) - return IdsJsonMarketLookup._from_dict(IdsJsonMarketType.PERP, group_address, market_data, tokens, group["quoteSymbol"]) + return IdsJsonMarketLookup._from_dict(IdsJsonMarketType.PERP, program_id, group_address, market_data, tokens, group["quoteSymbol"]) for market_data in group["spotMarkets"]: if market_data["key"] == str(address): tokens = IdsJsonMarketLookup._load_tokens(group["tokens"]) - return IdsJsonMarketLookup._from_dict(IdsJsonMarketType.SPOT, group_address, market_data, tokens, group["quoteSymbol"]) + return IdsJsonMarketLookup._from_dict(IdsJsonMarketType.SPOT, program_id, group_address, market_data, tokens, group["quoteSymbol"]) return None def all_markets(self) -> typing.Sequence[Market]: @@ -113,15 +115,16 @@ class IdsJsonMarketLookup(MarketLookup): for group in MangoConstants["groups"]: if group["cluster"] == self.cluster: group_address: PublicKey = PublicKey(group["publicKey"]) + program_id: PublicKey = PublicKey(group["mangoProgramId"]) for market_data in group["perpMarkets"]: tokens = IdsJsonMarketLookup._load_tokens(group["tokens"]) market = IdsJsonMarketLookup._from_dict( - IdsJsonMarketType.PERP, group_address, market_data, tokens, group["quoteSymbol"]) + IdsJsonMarketType.PERP, program_id, group_address, market_data, tokens, group["quoteSymbol"]) markets = [market] for market_data in group["spotMarkets"]: tokens = IdsJsonMarketLookup._load_tokens(group["tokens"]) market = IdsJsonMarketLookup._from_dict( - IdsJsonMarketType.SPOT, group_address, market_data, tokens, group["quoteSymbol"]) + IdsJsonMarketType.SPOT, program_id, group_address, market_data, tokens, group["quoteSymbol"]) markets = [market] return markets diff --git a/mango/market.py b/mango/market.py index 725eb8e..aa1888c 100644 --- a/mango/market.py +++ b/mango/market.py @@ -40,8 +40,9 @@ class InventorySource(enum.Enum): # class Market(metaclass=abc.ABCMeta): - def __init__(self, address: PublicKey, inventory_source: InventorySource, base: Token, quote: Token): + def __init__(self, program_id: PublicKey, address: PublicKey, inventory_source: InventorySource, base: Token, quote: Token): self.logger: logging.Logger = logging.getLogger(self.__class__.__name__) + self.program_id: PublicKey = program_id self.address: PublicKey = address self.inventory_source: InventorySource = inventory_source self.base: Token = base diff --git a/mango/marketoperations.py b/mango/marketoperations.py index 3e77036..f0b2d73 100644 --- a/mango/marketoperations.py +++ b/mango/marketoperations.py @@ -19,7 +19,9 @@ import logging import typing from decimal import Decimal +from solana.publickey import PublicKey +from .constants import SYSTEM_PROGRAM_ADDRESS from .orders import Order @@ -48,8 +50,6 @@ from .orders import Order # market_operations.place_order(order) # ``` # - - class MarketOperations(metaclass=abc.ABCMeta): def __init__(self): self.logger: logging.Logger = logging.getLogger(self.__class__.__name__) @@ -78,6 +78,14 @@ class MarketOperations(metaclass=abc.ABCMeta): def crank(self, limit: Decimal = Decimal(32)) -> typing.Sequence[str]: raise NotImplementedError("MarketOperations.crank() is not implemented on the base type.") + @abc.abstractmethod + def create_openorders(self) -> PublicKey: + raise NotImplementedError("MarketOperations.create_openorders() is not implemented on the base type.") + + @abc.abstractmethod + def ensure_openorders(self) -> PublicKey: + raise NotImplementedError("MarketOperations.ensure_openorders() is not implemented on the base type.") + def __repr__(self) -> str: return f"{self}" @@ -87,7 +95,6 @@ class MarketOperations(metaclass=abc.ABCMeta): # A null, no-op, dry-run trade executor that can be plugged in anywhere a `MarketOperations` # is expected, but which will not actually trade. # - class NullMarketOperations(MarketOperations): def __init__(self, market_name: str): super().__init__() @@ -113,5 +120,11 @@ class NullMarketOperations(MarketOperations): def crank(self, limit: Decimal = Decimal(32)) -> typing.Sequence[str]: return [] + def create_openorders(self) -> PublicKey: + return SYSTEM_PROGRAM_ADDRESS + + def ensure_openorders(self) -> PublicKey: + return SYSTEM_PROGRAM_ADDRESS + def __str__(self) -> str: return f"""Β« π™½πšžπš•πš•π™Ύπš›πšπšŽπš›π™Ώπš•πšŠπšŒπšŽπš› [{self.market_name}] Β»""" diff --git a/mango/oracles/serum/serum.py b/mango/oracles/serum/serum.py index 6432ac9..ff306a7 100644 --- a/mango/oracles/serum/serum.py +++ b/mango/oracles/serum/serum.py @@ -73,7 +73,8 @@ class SerumOracle(Oracle): context.client.commitment, context.client.skip_preflight, context.client.instruction_reporter) - mainnet_serum_market_lookup: SerumMarketLookup = SerumMarketLookup.load(SplTokenLookup.DefaultDataFilepath) + mainnet_serum_market_lookup: SerumMarketLookup = SerumMarketLookup.load( + context.dex_program_id, SplTokenLookup.DefaultDataFilepath) adjusted_market = self.market mainnet_adjusted_market: typing.Optional[Market] = mainnet_serum_market_lookup.find_by_symbol( self.market.symbol) @@ -110,7 +111,7 @@ class SerumOracleProvider(OracleProvider): def oracle_for_market(self, context: Context, market: Market) -> typing.Optional[Oracle]: loaded_market: Market = ensure_market_loaded(context, market) if isinstance(loaded_market, SpotMarket): - serum_market = SerumMarket(loaded_market.address, loaded_market.base, + serum_market = SerumMarket(context.dex_program_id, loaded_market.address, loaded_market.base, loaded_market.quote, loaded_market.underlying_serum_market) return SerumOracle(serum_market) elif isinstance(loaded_market, SerumMarket): diff --git a/mango/perpmarket.py b/mango/perpmarket.py index 4f19d7b..522e3a9 100644 --- a/mango/perpmarket.py +++ b/mango/perpmarket.py @@ -33,10 +33,9 @@ from .token import Token # # This class encapsulates our knowledge of a Mango perps market. # - class PerpMarket(Market): - def __init__(self, address: PublicKey, base: Token, quote: Token, underlying_perp_market: PerpMarketDetails): - super().__init__(address, InventorySource.ACCOUNT, base, quote) + def __init__(self, program_id: PublicKey, address: PublicKey, base: Token, quote: Token, underlying_perp_market: PerpMarketDetails): + super().__init__(program_id, address, InventorySource.ACCOUNT, base, quote) self.underlying_perp_market: PerpMarketDetails = underlying_perp_market self.lot_size_converter: LotSizeConverter = LotSizeConverter( base, underlying_perp_market.base_lot_size, quote, underlying_perp_market.quote_lot_size) @@ -82,7 +81,7 @@ class PerpMarket(Market): def __str__(self) -> str: underlying: str = f"{self.underlying_perp_market}".replace("\n", "\n ") - return f"""Β« π™ΏπšŽπš›πš™π™ΌπšŠπš›πš”πšŽπš {self.symbol} [{self.address}] + return f"""Β« π™ΏπšŽπš›πš™π™ΌπšŠπš›πš”πšŽπš {self.symbol} {self.address} [{self.program_id}] {underlying} Β»""" @@ -91,20 +90,19 @@ class PerpMarket(Market): # # This class holds information to load a `PerpMarket` object but doesn't automatically load it. # - class PerpMarketStub(Market): - def __init__(self, address: PublicKey, base: Token, quote: Token, group_address: PublicKey): - super().__init__(address, InventorySource.ACCOUNT, base, quote) + def __init__(self, program_id: PublicKey, address: PublicKey, base: Token, quote: Token, group_address: PublicKey): + super().__init__(program_id, address, InventorySource.ACCOUNT, base, quote) self.group_address: PublicKey = group_address def load(self, context: Context, group: typing.Optional[Group] = None) -> PerpMarket: actual_group: Group = group or Group.load(context, self.group_address) underlying_perp_market: PerpMarketDetails = PerpMarketDetails.load(context, self.address, actual_group) - return PerpMarket(self.address, self.base, self.quote, underlying_perp_market) + return PerpMarket(self.program_id, self.address, self.base, self.quote, underlying_perp_market) @property def symbol(self) -> str: return f"{self.base.symbol}-PERP" def __str__(self) -> str: - return f"Β« π™ΏπšŽπš›πš™π™ΌπšŠπš›πš”πšŽπšπš‚πšπšžπš‹ {self.symbol} [{self.address}] Β»" + return f"Β« π™ΏπšŽπš›πš™π™ΌπšŠπš›πš”πšŽπšπš‚πšπšžπš‹ {self.symbol} {self.address} [{self.program_id}] Β»" diff --git a/mango/perpmarketoperations.py b/mango/perpmarketoperations.py index a180407..93af71c 100644 --- a/mango/perpmarketoperations.py +++ b/mango/perpmarketoperations.py @@ -17,9 +17,11 @@ import typing from decimal import Decimal +from solana.publickey import PublicKey from .account import Account from .combinableinstructions import CombinableInstructions +from .constants import SYSTEM_PROGRAM_ADDRESS from .context import Context from .marketoperations import MarketOperations from .orders import Order @@ -32,8 +34,6 @@ from .wallet import Wallet # # This file deals with placing orders for Perps. # - - class PerpMarketOperations(MarketOperations): def __init__(self, market_name: str, context: Context, wallet: Wallet, market_instruction_builder: PerpMarketInstructionBuilder, @@ -80,6 +80,12 @@ class PerpMarketOperations(MarketOperations): crank = self.market_instruction_builder.build_crank_instructions(accounts_to_crank, limit) return (signers + crank).execute(self.context) + def create_openorders(self) -> PublicKey: + return SYSTEM_PROGRAM_ADDRESS + + def ensure_openorders(self) -> PublicKey: + return SYSTEM_PROGRAM_ADDRESS + def load_orders(self) -> typing.Sequence[Order]: return self.perp_market.orders(self.context) diff --git a/mango/serummarket.py b/mango/serummarket.py index db9ca93..3ef118a 100644 --- a/mango/serummarket.py +++ b/mango/serummarket.py @@ -32,11 +32,9 @@ from .token import Token # # This class encapsulates our knowledge of a Serum spot market. # - - class SerumMarket(Market): - def __init__(self, address: PublicKey, base: Token, quote: Token, underlying_serum_market: PySerumMarket): - super().__init__(address, InventorySource.SPL_TOKENS, base, quote) + def __init__(self, program_id: PublicKey, address: PublicKey, base: Token, quote: Token, underlying_serum_market: PySerumMarket): + super().__init__(program_id, address, InventorySource.SPL_TOKENS, base, quote) self.underlying_serum_market: PySerumMarket = underlying_serum_market def unprocessed_events(self, context: Context) -> typing.Sequence[SerumEvent]: @@ -53,7 +51,7 @@ class SerumMarket(Market): return list(map(Order.from_serum_order, itertools.chain(bids_orderbook.orders(), asks_orderbook.orders()))) def __str__(self) -> str: - return f"""Β« πš‚πšŽπš›πšžπš–π™ΌπšŠπš›πš”πšŽπš {self.symbol} [{self.address}] + return f"""Β« πš‚πšŽπš›πšžπš–π™ΌπšŠπš›πš”πšŽπš {self.symbol} {self.address} [{self.program_id}] Event Queue: {self.underlying_serum_market.state.event_queue()} Request Queue: {self.underlying_serum_market.state.request_queue()} Bids: {self.underlying_serum_market.state.bids()} @@ -67,16 +65,14 @@ class SerumMarket(Market): # # This class holds information to load a `SerumMarket` object but doesn't automatically load it. # - - class SerumMarketStub(Market): - def __init__(self, address: PublicKey, base: Token, quote: Token): - super().__init__(address, InventorySource.SPL_TOKENS, base, quote) + def __init__(self, program_id: PublicKey, address: PublicKey, base: Token, quote: Token): + super().__init__(program_id, address, InventorySource.SPL_TOKENS, base, quote) def load(self, context: Context) -> SerumMarket: underlying_serum_market: PySerumMarket = PySerumMarket.load( context.client.compatible_client, self.address, context.dex_program_id) - return SerumMarket(self.address, self.base, self.quote, underlying_serum_market) + return SerumMarket(self.program_id, self.address, self.base, self.quote, underlying_serum_market) def __str__(self) -> str: - return f"Β« πš‚πšŽπš›πšžπš–π™ΌπšŠπš›πš”πšŽπšπš‚πšπšžπš‹ {self.symbol} [{self.address}] Β»" + return f"Β« πš‚πšŽπš›πšžπš–π™ΌπšŠπš›πš”πšŽπšπš‚πšπšžπš‹ {self.symbol} {self.address} [{self.program_id}] Β»" diff --git a/mango/serummarketinstructionbuilder.py b/mango/serummarketinstructionbuilder.py index a20c2d5..c63f223 100644 --- a/mango/serummarketinstructionbuilder.py +++ b/mango/serummarketinstructionbuilder.py @@ -41,7 +41,6 @@ from .wallet import Wallet # existing data, requiring no fetches from Solana or other sources. All necessary data should all be loaded # on initial setup in the `load()` method. # - class SerumMarketInstructionBuilder(MarketInstructionBuilder): def __init__(self, context: Context, wallet: Wallet, serum_market: SerumMarket, raw_market: Market, base_token_account: TokenAccount, quote_token_account: TokenAccount, open_orders_address: typing.Optional[PublicKey], fee_discount_token_address: typing.Optional[PublicKey]): super().__init__() @@ -96,10 +95,7 @@ class SerumMarketInstructionBuilder(MarketInstructionBuilder): def build_place_order_instructions(self, order: Order) -> CombinableInstructions: ensure_open_orders = CombinableInstructions.empty() if self.open_orders_address is None: - ensure_open_orders = build_create_serum_open_orders_instructions( - self.context, self.wallet, self.raw_market) - - self.open_orders_address = ensure_open_orders.signers[0].public_key() + ensure_open_orders = self.build_create_openorders_instructions() serum_order_type: pyserum.enums.OrderType = order.order_type.to_serum() serum_side: pyserum.enums.Side = order.side.to_serum() @@ -134,5 +130,10 @@ class SerumMarketInstructionBuilder(MarketInstructionBuilder): return build_serum_consume_events_instructions(self.context, self.serum_market.address, self.raw_market.state.event_queue(), limited_open_orders_addresses, int(limit)) + def build_create_openorders_instructions(self) -> CombinableInstructions: + create_open_orders = build_create_serum_open_orders_instructions(self.context, self.wallet, self.raw_market) + self.open_orders_address = create_open_orders.signers[0].public_key() + return create_open_orders + def __str__(self) -> str: return """Β« πš‚πšŽπš›πšžπš–π™ΌπšŠπš›πš”πšŽπšπ™Έπš—πšœπšπš›πšžπšŒπšπš’πš˜πš—π™±πšžπš’πš•πšπšŽπš› Β»""" diff --git a/mango/serummarketlookup.py b/mango/serummarketlookup.py index 1b3bb14..2555b83 100644 --- a/mango/serummarketlookup.py +++ b/mango/serummarketlookup.py @@ -49,18 +49,17 @@ from .token import Token # the list, check if the item has the optional `extensions` attribute, and in there see if # there is a name-value pair for the particular market we're interested in. Also, the # current file only lists USDC and USDT markets, so that's all we can support this way. - - class SerumMarketLookup(MarketLookup): - def __init__(self, token_data: typing.Dict) -> None: + def __init__(self, program_id: PublicKey, token_data: typing.Dict) -> None: super().__init__() + self.program_id: PublicKey = program_id self.token_data: typing.Dict = token_data @staticmethod - def load(token_data_filename: str) -> "SerumMarketLookup": + def load(program_id: PublicKey, token_data_filename: str) -> "SerumMarketLookup": with open(token_data_filename) as json_file: token_data = json.load(json_file) - return SerumMarketLookup(token_data) + return SerumMarketLookup(program_id, token_data) @staticmethod def _find_data_by_symbol(symbol: str, token_data: typing.Dict) -> typing.Optional[typing.Dict]: @@ -122,7 +121,7 @@ class SerumMarketLookup(MarketLookup): f"Could not find market with quote token '{quote.symbol}'. Only markets based on USDC or USDT are supported.") return None - return SerumMarketStub(market_address, base, quote) + return SerumMarketStub(self.program_id, market_address, base, quote) def find_by_address(self, address: PublicKey) -> typing.Optional[Market]: address_string: str = str(address) @@ -139,7 +138,7 @@ class SerumMarketLookup(MarketLookup): raise Exception("Could not load token data for USDC (which should always be present).") quote = Token(quote_data["symbol"], quote_data["name"], PublicKey( quote_data["address"]), Decimal(quote_data["decimals"])) - return SerumMarketStub(market_address, base, quote) + return SerumMarketStub(self.program_id, market_address, base, quote) if "serumV3Usdt" in token_data["extensions"]: if token_data["extensions"]["serumV3Usdt"] == address_string: market_address_string = token_data["extensions"]["serumV3Usdt"] @@ -151,7 +150,7 @@ class SerumMarketLookup(MarketLookup): raise Exception("Could not load token data for USDT (which should always be present).") quote = Token(quote_data["symbol"], quote_data["name"], PublicKey( quote_data["address"]), Decimal(quote_data["decimals"])) - return SerumMarketStub(market_address, base, quote) + return SerumMarketStub(self.program_id, market_address, base, quote) return None def all_markets(self) -> typing.Sequence[Market]: @@ -166,12 +165,12 @@ class SerumMarketLookup(MarketLookup): market_address = PublicKey(market_address_string) base = Token(token_data["symbol"], token_data["name"], PublicKey( token_data["address"]), Decimal(token_data["decimals"])) - all_markets += [SerumMarketStub(market_address, base, usdc)] + all_markets += [SerumMarketStub(self.program_id, market_address, base, usdc)] if "serumV3Usdt" in token_data["extensions"]: market_address_string = token_data["extensions"]["serumV3Usdt"] market_address = PublicKey(market_address_string) base = Token(token_data["symbol"], token_data["name"], PublicKey( token_data["address"]), Decimal(token_data["decimals"])) - all_markets += [SerumMarketStub(market_address, base, usdt)] + all_markets += [SerumMarketStub(self.program_id, market_address, base, usdt)] return all_markets diff --git a/mango/serummarketoperations.py b/mango/serummarketoperations.py index 538f308..469ea0c 100644 --- a/mango/serummarketoperations.py +++ b/mango/serummarketoperations.py @@ -31,9 +31,8 @@ from .wallet import Wallet # # πŸ₯­ SerumMarketOperations class # -# This class puts trades on the Serum orderbook. It doesn't do anything complicated. +# This class performs standard operations on the Serum orderbook. # - class SerumMarketOperations(MarketOperations): def __init__(self, context: Context, wallet: Wallet, serum_market: SerumMarket, market_instruction_builder: SerumMarketInstructionBuilder): super().__init__() @@ -78,6 +77,19 @@ class SerumMarketOperations(MarketOperations): crank = self._build_crank(limit) return (signers + crank).execute(self.context) + def create_openorders(self) -> PublicKey: + signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) + create_open_orders = self.market_instruction_builder.build_create_openorders_instructions() + open_orders_address = create_open_orders.signers[0].public_key() + (signers + create_open_orders).execute(self.context) + + return open_orders_address + + def ensure_openorders(self) -> PublicKey: + if self.market_instruction_builder.open_orders_address is not None: + return self.market_instruction_builder.open_orders_address + return self.create_openorders() + def load_orders(self) -> typing.Sequence[Order]: return self.serum_market.orders(self.context) diff --git a/mango/spotmarket.py b/mango/spotmarket.py index 62ac671..e220348 100644 --- a/mango/spotmarket.py +++ b/mango/spotmarket.py @@ -33,11 +33,9 @@ from .token import Token # # This class encapsulates our knowledge of a Serum spot market. # - - class SpotMarket(Market): - def __init__(self, address: PublicKey, base: Token, quote: Token, group: Group, underlying_serum_market: PySerumMarket): - super().__init__(address, InventorySource.ACCOUNT, base, quote) + def __init__(self, program_id: PublicKey, address: PublicKey, base: Token, quote: Token, group: Group, underlying_serum_market: PySerumMarket): + super().__init__(program_id, address, InventorySource.ACCOUNT, base, quote) self.group: Group = group self.underlying_serum_market: PySerumMarket = underlying_serum_market @@ -55,7 +53,7 @@ class SpotMarket(Market): return list(map(Order.from_serum_order, itertools.chain(bids_orderbook.orders(), asks_orderbook.orders()))) def __str__(self) -> str: - return f"""Β« πš‚πš™πš˜πšπ™ΌπšŠπš›πš”πšŽπš {self.symbol} [{self.address}] + return f"""Β« πš‚πš™πš˜πšπ™ΌπšŠπš›πš”πšŽπš {self.symbol} {self.address} [{self.program_id}] Event Queue: {self.underlying_serum_market.state.event_queue()} Request Queue: {self.underlying_serum_market.state.request_queue()} Bids: {self.underlying_serum_market.state.bids()} @@ -72,15 +70,15 @@ class SpotMarket(Market): class SpotMarketStub(Market): - def __init__(self, address: PublicKey, base: Token, quote: Token, group_address: PublicKey): - super().__init__(address, InventorySource.ACCOUNT, base, quote) + def __init__(self, program_id: PublicKey, address: PublicKey, base: Token, quote: Token, group_address: PublicKey): + super().__init__(program_id, address, InventorySource.ACCOUNT, base, quote) self.group_address: PublicKey = group_address def load(self, context: Context, group: typing.Optional[Group]) -> SpotMarket: actual_group: Group = group or Group.load(context, self.group_address) underlying_serum_market: PySerumMarket = PySerumMarket.load( context.client.compatible_client, self.address, context.dex_program_id) - return SpotMarket(self.address, self.base, self.quote, actual_group, underlying_serum_market) + return SpotMarket(self.program_id, self.address, self.base, self.quote, actual_group, underlying_serum_market) def __str__(self) -> str: - return f"Β« πš‚πš™πš˜πšπ™ΌπšŠπš›πš”πšŽπšπš‚πšπšžπš‹ {self.symbol} [{self.address}] Β»" + return f"Β« πš‚πš™πš˜πšπ™ΌπšŠπš›πš”πšŽπšπš‚πšπšžπš‹ {self.symbol} {self.address} [{self.program_id}] Β»" diff --git a/mango/spotmarketoperations.py b/mango/spotmarketoperations.py index bab0872..7cbe379 100644 --- a/mango/spotmarketoperations.py +++ b/mango/spotmarketoperations.py @@ -35,7 +35,6 @@ from .wallet import Wallet # # This class puts trades on the Serum orderbook. It doesn't do anything complicated. # - class SpotMarketOperations(MarketOperations): def __init__(self, context: Context, wallet: Wallet, group: Group, account: Account, spot_market: SpotMarket, market_instruction_builder: SpotMarketInstructionBuilder): super().__init__() @@ -86,6 +85,24 @@ class SpotMarketOperations(MarketOperations): crank = self._build_crank(limit, add_self=False) return (signers + crank).execute(self.context) + def create_openorders(self) -> PublicKey: + signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) + create_open_orders: CombinableInstructions = self.market_instruction_builder.build_create_openorders_instructions() + open_orders_address: PublicKey = create_open_orders.signers[0].public_key() + (signers + create_open_orders).execute(self.context) + + # This line is a little nasty. Now that we know we have an OpenOrders account at this address, update + # the Account so that future uses (like later in this method) have access to it in the right place. + self.account.update_spot_open_orders_for_market(self.market_index, open_orders_address) + + return open_orders_address + + def ensure_openorders(self) -> PublicKey: + existing: typing.Optional[PublicKey] = self.account.spot_open_orders[self.market_index] + if existing is not None: + return existing + return self.create_openorders() + def load_orders(self) -> typing.Sequence[Order]: return self.spot_market.orders(self.context) @@ -96,14 +113,6 @@ class SpotMarketOperations(MarketOperations): all_orders = self.spot_market.orders(self.context) return list([o for o in all_orders if o.owner == self.open_orders_address]) - def create_openorders_for_market(self) -> PublicKey: - signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) - create_open_orders = self.market_instruction_builder.build_create_openorders_instructions() - open_orders_address = create_open_orders.signers[0].public_key() - (signers + create_open_orders).execute(self.context) - - return open_orders_address - def _build_crank(self, limit: Decimal = Decimal(32), add_self: bool = False) -> CombinableInstructions: open_orders_to_crank: typing.List[PublicKey] = [] for event in self.spot_market.unprocessed_events(self.context): diff --git a/mango/watchers.py b/mango/watchers.py index 22d32e8..ec1a327 100644 --- a/mango/watchers.py +++ b/mango/watchers.py @@ -70,13 +70,9 @@ def build_spot_open_orders_watcher(context: Context, manager: WebSocketSubscript context, wallet, spot_market.group, account, spot_market) market_operations: SpotMarketOperations = SpotMarketOperations( context, wallet, spot_market.group, account, spot_market, spot_market_instruction_builder) - open_orders_address = market_operations.create_openorders_for_market() + open_orders_address = market_operations.create_openorders() logging.info(f"Created {spot_market.symbol} OpenOrders at: {open_orders_address}") - # This line is a little nasty. Now that we know we have an OpenOrders account at this address, update - # the Account so that future uses (like later in this method) have access to it in the right place. - account.update_spot_open_orders_for_market(market_index, open_orders_address) - spot_open_orders_subscription = WebSocketAccountSubscription[OpenOrders]( context, open_orders_address, lambda account_info: OpenOrders.parse(account_info, spot_market.base.decimals, spot_market.quote.decimals)) manager.add(spot_open_orders_subscription) diff --git a/tests/fakes.py b/tests/fakes.py index 7ecf88a..ca1a78d 100644 --- a/tests/fakes.py +++ b/tests/fakes.py @@ -82,7 +82,7 @@ def fake_market() -> market.Market: def fake_spot_market_stub() -> mango.SpotMarketStub: - return mango.SpotMarketStub(fake_seeded_public_key("spot market"), fake_token("BASE"), fake_token("QUOTE"), fake_seeded_public_key("group address")) + return mango.SpotMarketStub(fake_seeded_public_key("program ID"), fake_seeded_public_key("spot market"), fake_token("BASE"), fake_token("QUOTE"), fake_seeded_public_key("group address")) def fake_token_account() -> mango.TokenAccount: diff --git a/tests/test_marketlookup.py b/tests/test_marketlookup.py index 1c872ad..bf91f50 100644 --- a/tests/test_marketlookup.py +++ b/tests/test_marketlookup.py @@ -2,6 +2,8 @@ from .context import mango from solana.publickey import PublicKey +from .fakes import fake_seeded_public_key + def test_serum_market_lookup(): data = { @@ -104,7 +106,7 @@ def test_serum_market_lookup(): } ] } - actual = mango.SerumMarketLookup(data) + actual = mango.SerumMarketLookup(fake_seeded_public_key("program ID"), data) assert actual is not None assert actual.logger is not None assert actual.find_by_symbol("ETH/USDT") is not None @@ -114,7 +116,8 @@ def test_serum_market_lookup(): def test_serum_market_lookups_with_full_data(): - market_lookup = mango.SerumMarketLookup.load(mango.SplTokenLookup.DefaultDataFilepath) + market_lookup = mango.SerumMarketLookup.load(fake_seeded_public_key( + "program ID"), mango.SplTokenLookup.DefaultDataFilepath) eth_usdt = market_lookup.find_by_symbol("ETH/USDT") assert eth_usdt.base.symbol == "ETH" assert eth_usdt.quote.symbol == "USDT" diff --git a/tests/test_spotmarket.py b/tests/test_spotmarket.py index 069bae8..ab7556d 100644 --- a/tests/test_spotmarket.py +++ b/tests/test_spotmarket.py @@ -5,14 +5,16 @@ from decimal import Decimal def test_spot_market_stub_constructor(): + program_id = fake_seeded_public_key("program ID") address = fake_seeded_public_key("spot market address") base = mango.Token("BASE", "Base Token", fake_seeded_public_key("base token"), Decimal(7)) quote = mango.Token("QUOTE", "Quote Token", fake_seeded_public_key("quote token"), Decimal(9)) group_address = fake_seeded_public_key("group address") - actual = mango.SpotMarketStub(address, base, quote, group_address) + actual = mango.SpotMarketStub(program_id, address, base, quote, group_address) assert actual is not None assert actual.logger is not None assert actual.base == base assert actual.quote == quote assert actual.address == address + assert actual.program_id == program_id assert actual.group_address == group_address