# # ⚠ 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 argparse import logging import os from solana.publickey import PublicKey from .constants import MangoConstants from .context import Context from .idsjsonmarketlookup import IdsJsonMarketLookup from .idsjsontokenlookup import IdsJsonTokenLookup from .marketlookup import CompoundMarketLookup, MarketLookup from .serummarketlookup import SerumMarketLookup from .spltokenlookup import SplTokenLookup from .tokenlookup import TokenLookup, CompoundTokenLookup # # 🥭 ContextBuilder # # ## Environment Variables # # It's possible to override the values in the `Context` variables provided. This can be easier than creating # the `Context` in code or introducing dependencies and configuration. # # The following environment variables are read: # * CLUSTER (defaults to: mainnet-beta) # * CLUSTER_URL (defaults to URL for RPC server for CLUSTER defined in `ids.json`) # * GROUP_NAME (defaults to: BTC_ETH_USDT) # default_name: str = os.environ.get("NAME") or "Mango Explorer" default_skip_preflight: bool = False _default_group_data = MangoConstants["groups"][0] default_cluster: str = os.environ.get("CLUSTER") or _default_group_data["cluster"] default_cluster_url: str = os.environ.get("CLUSTER_URL") or MangoConstants["cluster_urls"][default_cluster] default_program_id: PublicKey = PublicKey(_default_group_data["mangoProgramId"]) default_dex_program_id: PublicKey = PublicKey(_default_group_data["serumProgramId"]) default_group_name: str = os.environ.get("GROUP_NAME") or _default_group_data["name"] default_group_id: PublicKey = PublicKey(_default_group_data["publicKey"]) # # 🥭 ContextBuilder class # # A `ContextBuilder` class to allow building `Context` objects without introducing circular dependencies. # class ContextBuilder: # Configuring a `Context` is a common operation for command-line programs and can involve a # lot of duplicate code. # # This function centralises some of it to ensure consistency and readability. # @staticmethod def add_command_line_parameters(parser: argparse.ArgumentParser, logging_default=logging.INFO) -> None: parser.add_argument("--name", type=str, default=default_name, help="Name of the program (used in reports and alerts)") parser.add_argument("--cluster", type=str, default=default_cluster, help="Solana RPC cluster name") parser.add_argument("--cluster-url", type=str, default=default_cluster_url, help="Solana RPC cluster URL") parser.add_argument("--skip-preflight", default=default_skip_preflight, action="store_true", help="Skip Solana pre-flight checks") parser.add_argument("--program-id", type=PublicKey, default=default_program_id, help="Mango program ID/address") parser.add_argument("--dex-program-id", type=PublicKey, default=default_dex_program_id, help="DEX program ID/address") parser.add_argument("--group-name", type=str, default=default_group_name, help="Mango group name") parser.add_argument("--group-id", type=PublicKey, default=default_group_id, help="Mango group ID/address") parser.add_argument("--token-data-file", type=str, default=SplTokenLookup.DefaultDataFilepath, help="data file that contains token symbols, names, mints and decimals (format is same as https://raw.githubusercontent.com/solana-labs/token-list/main/src/tokens/solana.tokenlist.json)") # This isn't really a Context thing but we don't have a better place for it (yet) and we # don't want to duplicate it in every command. parser.add_argument("--log-level", default=logging_default, type=lambda level: getattr(logging, level), help="level of verbosity to log (possible values: DEBUG, INFO, WARNING, ERROR, CRITICAL)") # This function is the converse of `add_command_line_parameters()` - it takes # an argument of parsed command-line parameters and expects to see the ones it added # to that collection in the `add_command_line_parameters()` call. # # It then uses those parameters to create a properly-configured `Context` object. # @staticmethod def from_command_line_parameters(args: argparse.Namespace) -> "Context": # Here we should have values for all our parameters (because they'll either be specified # on the command-line or will be the default_* value) but we may be in the situation where # a group name is specified but not a group ID, and in that case we want to look up the # group ID. # # In that situation, the group_name will not be default_group_name but the group_id will # still be default_group_id. In that situation we want to override what we were passed # as the group_id. name: str = args.name group_name: str = args.group_name cluster: str = args.cluster cluster_url: str = args.cluster_url skip_preflight: bool = args.skip_preflight token_filename: str = args.token_data_file group_id: PublicKey = args.group_id program_id: PublicKey = args.program_id dex_program_id: PublicKey = args.dex_program_id return ContextBuilder._build(name, cluster, cluster_url, skip_preflight, group_name, group_id, program_id, dex_program_id, token_filename) @staticmethod def default(): return ContextBuilder._build(default_name, default_cluster, default_cluster_url, default_skip_preflight, default_group_name, default_group_id, default_program_id, default_dex_program_id, SplTokenLookup.DefaultDataFilepath) # This function is the converse of `add_command_line_parameters()` - it takes # an argument of parsed command-line parameters and expects to see the ones it added # to that collection in the `add_command_line_parameters()` call. # # It then uses those parameters to create a properly-configured `Context` object. # @staticmethod def _build(name: str, cluster: str, cluster_url: str, skip_preflight: bool, group_name: str, group_id: PublicKey, program_id: PublicKey, dex_program_id: PublicKey, token_filename: str) -> "Context": ids_json_token_lookup: TokenLookup = IdsJsonTokenLookup(cluster, group_name) all_token_lookup = ids_json_token_lookup if cluster == "mainnet-beta": mainnet_spl_token_lookup: TokenLookup = SplTokenLookup.load(token_filename) all_token_lookup = CompoundTokenLookup([ids_json_token_lookup, mainnet_spl_token_lookup]) elif cluster == "devnet": devnet_token_filename = token_filename.rsplit('.', 1)[0] + ".devnet.json" devnet_spl_token_lookup: TokenLookup = SplTokenLookup.load(devnet_token_filename) all_token_lookup = CompoundTokenLookup([ids_json_token_lookup, devnet_spl_token_lookup]) token_lookup: TokenLookup = all_token_lookup 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) 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) all_market_lookup = CompoundMarketLookup([ids_json_market_lookup, devnet_serum_market_lookup]) market_lookup: MarketLookup = all_market_lookup if (group_name != default_group_name) and (group_id == default_group_id): for group in MangoConstants["groups"]: if group["cluster"] == cluster and group["name"].upper() == group_name.upper(): group_id = PublicKey(group["publicKey"]) elif (cluster != default_cluster) and (group_id == default_group_id): for group in MangoConstants["groups"]: if group["cluster"] == cluster and group["name"].upper() == group_name.upper(): group_id = PublicKey(group["publicKey"]) # Same problem here, but with cluster names and URLs. We want someone to be able to change the # cluster just by changing the cluster name. if (cluster != default_cluster) and (cluster_url == default_cluster_url): cluster_url = MangoConstants["cluster_urls"][cluster] return Context(name, cluster, cluster_url, skip_preflight, program_id, dex_program_id, group_name, group_id, token_lookup, market_lookup)