Added ensure-account and ensure-open-orders commands.

This commit is contained in:
Geoff Taylor 2021-08-19 11:00:39 +01:00
parent f8ce3d52e0
commit ec37c14d03
21 changed files with 208 additions and 85 deletions

40
bin/ensure-account Executable file
View File

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

43
bin/ensure-open-orders Executable file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}] »"""

View File

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

View File

@ -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}] »"

View File

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

View File

@ -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}] »"

View File

@ -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 """« 𝚂𝚎𝚛𝚞𝚖𝙼𝚊𝚛𝚔𝚎𝚝𝙸𝚗𝚜𝚝𝚛𝚞𝚌𝚝𝚒𝚘𝚗𝙱𝚞𝚒𝚕𝚍𝚎𝚛 »"""

View File

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

View File

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

View File

@ -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}] »"

View File

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

View File

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

View File

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

View File

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

View File

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