2021-07-23 02:20:44 -07:00
# # ⚠ 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
2021-08-26 10:03:42 -07:00
import copy
2021-09-30 00:54:12 -07:00
import logging
2021-07-23 02:20:44 -07:00
import os
2021-08-25 09:06:00 -07:00
import typing
2021-07-23 02:20:44 -07:00
2021-09-13 04:17:19 -07:00
from decimal import Decimal
2021-07-23 02:20:44 -07:00
from solana . publickey import PublicKey
2021-08-26 10:03:42 -07:00
from . client import BetterClient
2021-12-15 08:03:31 -08:00
from . constants import MangoConstants , DATA_PATH
2021-07-23 02:20:44 -07:00
from . context import Context
from . idsjsonmarketlookup import IdsJsonMarketLookup
2021-11-08 03:39:09 -08:00
from . instrumentlookup import InstrumentLookup , CompoundInstrumentLookup , IdsJsonTokenLookup , NonSPLInstrumentLookup , SPLTokenLookup
2021-07-23 02:20:44 -07:00
from . marketlookup import CompoundMarketLookup , MarketLookup
from . serummarketlookup import SerumMarketLookup
# # 🥭 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:
2021-08-25 09:06:00 -07:00
# * NAME
# * CLUSTER
# * CLUSTER_URL
# * GROUP_NAME
# * GROUP_ADDRESS
2021-08-26 02:31:02 -07:00
# * MANGO_PROGRAM_ADDRESS
2021-08-25 09:06:00 -07:00
# * SERUM_PROGRAM_ADDRESS
2021-07-23 02:20:44 -07:00
# # 🥭 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
2021-09-13 06:05:19 -07:00
def add_command_line_parameters ( parser : argparse . ArgumentParser ) - > None :
2021-08-25 09:06:00 -07:00
parser . add_argument ( " --name " , type = str , default = " Mango Explorer " ,
2021-08-09 02:27:47 -07:00
help = " Name of the program (used in reports and alerts) " )
2021-08-26 02:31:02 -07:00
parser . add_argument ( " --cluster-name " , type = str , default = None , help = " Solana RPC cluster name " )
2021-12-05 08:38:49 -08:00
parser . add_argument ( " --cluster-url " , type = str , action = " append " , default = [ ] ,
help = " Solana RPC cluster URL (can be specified multiple times to provide failover when one errors) " )
2021-08-25 09:06:00 -07:00
parser . add_argument ( " --group-name " , type = str , default = None , help = " Mango group name " )
2021-08-26 02:31:02 -07:00
parser . add_argument ( " --group-address " , type = PublicKey , default = None , help = " Mango group address " )
parser . add_argument ( " --mango-program-address " , type = PublicKey , default = None , help = " Mango program address " )
parser . add_argument ( " --serum-program-address " , type = PublicKey , default = None , help = " Serum program address " )
parser . add_argument ( " --skip-preflight " , default = False , action = " store_true " , help = " Skip pre-flight checks " )
2021-09-16 07:36:52 -07:00
parser . add_argument ( " --commitment " , type = str , default = None ,
help = " Commitment to use when sending transactions (can be ' finalized ' , ' confirmed ' or ' processed ' ) " )
2021-09-15 11:27:07 -07:00
parser . add_argument ( " --encoding " , type = str , default = None ,
help = " Encoding to request when receiving data from Solana (options are ' base58 ' (slow), ' base64 ' , ' base64+zstd ' , or ' jsonParsed ' ) " )
2021-12-01 08:14:30 -08:00
parser . add_argument ( " --blockhash-cache-duration " , type = int ,
help = " How long (in seconds) to cache ' recent ' blockhashes " )
2021-12-04 05:44:22 -08:00
parser . add_argument ( " --stale-data-pause-before-retry " , type = Decimal ,
help = " How long (in seconds, e.g. 0.1) to pause after retrieving stale data before retrying " )
parser . add_argument ( " --stale-data-maximum-retries " , type = int ,
help = " How many times to retry fetching data after being given stale data before giving up " )
2021-09-13 04:17:19 -07:00
parser . add_argument ( " --gma-chunk-size " , type = Decimal , default = None ,
help = " Maximum number of addresses to send in a single call to getMultipleAccounts() " )
parser . add_argument ( " --gma-chunk-pause " , type = Decimal , default = None ,
help = " number of seconds to pause between successive getMultipleAccounts() calls to avoid rate limiting " )
2021-07-23 02:20:44 -07:00
2021-11-08 03:39:09 -08:00
parser . add_argument ( " --token-data-file " , type = str , default = SPLTokenLookup . DefaultDataFilepath ,
2021-07-23 02:20:44 -07:00
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 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
2021-08-26 10:03:42 -07:00
def from_command_line_parameters ( args : argparse . Namespace ) - > Context :
2021-08-25 09:06:00 -07:00
name : typing . Optional [ str ] = args . name
2021-08-26 02:31:02 -07:00
cluster_name : typing . Optional [ str ] = args . cluster_name
2021-12-05 08:38:49 -08:00
cluster_urls : typing . Optional [ typing . Sequence [ str ] ] = args . cluster_url
2021-08-26 02:31:02 -07:00
group_name : typing . Optional [ str ] = args . group_name
group_address : typing . Optional [ PublicKey ] = args . group_address
mango_program_address : typing . Optional [ PublicKey ] = args . mango_program_address
serum_program_address : typing . Optional [ PublicKey ] = args . serum_program_address
2021-08-25 09:06:00 -07:00
skip_preflight : bool = bool ( args . skip_preflight )
2021-09-16 07:36:52 -07:00
commitment : typing . Optional [ str ] = args . commitment
2021-09-15 11:27:07 -07:00
encoding : typing . Optional [ str ] = args . encoding
2021-12-01 08:14:30 -08:00
blockhash_cache_duration : typing . Optional [ int ] = args . blockhash_cache_duration
2021-12-04 05:44:22 -08:00
stale_data_pause_before_retry : typing . Optional [ Decimal ] = args . stale_data_pause_before_retry
stale_data_maximum_retries : typing . Optional [ int ] = args . stale_data_maximum_retries
2021-09-13 04:17:19 -07:00
gma_chunk_size : typing . Optional [ Decimal ] = args . gma_chunk_size
gma_chunk_pause : typing . Optional [ Decimal ] = args . gma_chunk_pause
2021-07-23 02:20:44 -07:00
token_filename : str = args . token_data_file
2021-12-04 05:44:22 -08:00
# Do this here so build() only ever has to handle the sequence of retry times. (It gets messy
# passing around the sequnce *plus* the data to reconstruct it for build().)
2022-01-11 11:35:12 -08:00
actual_stale_data_pauses_before_retry : typing . Sequence [ float ]
if stale_data_maximum_retries == 0 :
# Stale data checking is explicitly disabled
actual_stale_data_pauses_before_retry = [ ]
else :
retries : int = stale_data_maximum_retries or 10
pause : Decimal = stale_data_pause_before_retry or Decimal ( " 0.1 " )
actual_stale_data_pauses_before_retry = [ float ( pause ) ] * retries
2021-12-04 05:44:22 -08:00
2021-12-05 08:38:49 -08:00
context : Context = ContextBuilder . build ( name , cluster_name , cluster_urls , skip_preflight , commitment ,
2021-12-04 05:44:22 -08:00
encoding , blockhash_cache_duration ,
actual_stale_data_pauses_before_retry ,
group_name , group_address , mango_program_address ,
serum_program_address , gma_chunk_size , gma_chunk_pause ,
token_filename )
2021-09-30 00:54:12 -07:00
logging . debug ( f " { context } " )
return context
2021-07-23 02:20:44 -07:00
@staticmethod
2021-11-09 05:23:36 -08:00
def default ( ) - > Context :
2021-10-11 09:08:54 -07:00
return ContextBuilder . build ( )
2021-08-25 09:06:00 -07:00
@staticmethod
def from_group_name ( context : Context , group_name : str ) - > Context :
2021-12-05 08:38:49 -08:00
return ContextBuilder . build ( context . name , context . client . cluster_name , context . client . cluster_urls ,
2021-10-11 09:08:54 -07:00
context . client . skip_preflight , context . client . commitment ,
2021-12-01 08:14:30 -08:00
context . client . encoding , context . client . blockhash_cache_duration ,
2021-12-04 05:44:22 -08:00
context . client . stale_data_pauses_before_retry ,
2021-12-01 08:14:30 -08:00
group_name , None , None , None ,
context . gma_chunk_size , context . gma_chunk_pause ,
2021-11-08 03:39:09 -08:00
SPLTokenLookup . DefaultDataFilepath )
2021-08-25 09:06:00 -07:00
@staticmethod
def forced_to_devnet ( context : Context ) - > Context :
2021-08-26 02:31:02 -07:00
cluster_name : str = " devnet "
cluster_url : str = MangoConstants [ " cluster_urls " ] [ cluster_name ]
2021-08-26 10:03:42 -07:00
fresh_context = copy . copy ( context )
fresh_context . client = BetterClient . from_configuration ( context . name ,
cluster_name ,
2021-12-05 08:38:49 -08:00
[ cluster_url ] ,
2021-08-26 10:03:42 -07:00
context . client . commitment ,
context . client . skip_preflight ,
2021-09-15 11:27:07 -07:00
context . client . encoding ,
2021-12-01 08:14:30 -08:00
context . client . blockhash_cache_duration ,
2021-12-04 05:44:22 -08:00
context . client . stale_data_pauses_before_retry ,
2021-09-15 11:27:07 -07:00
context . client . instruction_reporter )
2021-08-26 10:03:42 -07:00
return fresh_context
2021-08-25 09:06:00 -07:00
@staticmethod
def forced_to_mainnet_beta ( context : Context ) - > Context :
2021-08-26 02:31:02 -07:00
cluster_name : str = " mainnet "
cluster_url : str = MangoConstants [ " cluster_urls " ] [ cluster_name ]
2021-08-26 10:03:42 -07:00
fresh_context = copy . copy ( context )
fresh_context . client = BetterClient . from_configuration ( context . name ,
cluster_name ,
2021-12-05 08:38:49 -08:00
[ cluster_url ] ,
2021-08-26 10:03:42 -07:00
context . client . commitment ,
context . client . skip_preflight ,
2021-09-15 11:27:07 -07:00
context . client . encoding ,
2021-12-01 08:14:30 -08:00
context . client . blockhash_cache_duration ,
2021-12-04 05:44:22 -08:00
context . client . stale_data_pauses_before_retry ,
2021-09-15 11:27:07 -07:00
context . client . instruction_reporter )
2021-08-26 10:03:42 -07:00
return fresh_context
2021-07-23 02:20:44 -07:00
@staticmethod
2021-10-11 09:08:54 -07:00
def build ( name : typing . Optional [ str ] = None , cluster_name : typing . Optional [ str ] = None ,
2021-12-05 08:38:49 -08:00
cluster_urls : typing . Optional [ typing . Sequence [ str ] ] = None , skip_preflight : bool = False ,
2021-12-01 08:14:30 -08:00
commitment : typing . Optional [ str ] = None , encoding : typing . Optional [ str ] = None ,
blockhash_cache_duration : typing . Optional [ int ] = None ,
2021-12-04 05:44:22 -08:00
stale_data_pauses_before_retry : typing . Optional [ typing . Sequence [ float ] ] = None ,
2021-10-11 09:08:54 -07:00
group_name : typing . Optional [ str ] = None , group_address : typing . Optional [ PublicKey ] = None ,
program_address : typing . Optional [ PublicKey ] = None , serum_program_address : typing . Optional [ PublicKey ] = None ,
gma_chunk_size : typing . Optional [ Decimal ] = None , gma_chunk_pause : typing . Optional [ Decimal ] = None ,
2021-11-08 03:39:09 -08:00
token_filename : str = SPLTokenLookup . DefaultDataFilepath ) - > " Context " :
2021-12-05 08:38:49 -08:00
def __public_key_or_none ( address : typing . Optional [ str ] ) - > typing . Optional [ PublicKey ] :
2021-08-25 09:06:00 -07:00
if address is not None and address != " " :
return PublicKey ( address )
return None
2021-08-27 12:37:23 -07:00
# The first group is only used to determine the default cluster if it is not otherwise specified.
first_group_data = MangoConstants [ " groups " ] [ 0 ]
2021-08-25 09:06:00 -07:00
actual_name : str = name or os . environ . get ( " NAME " ) or " Mango Explorer "
2021-08-27 12:37:23 -07:00
actual_cluster : str = cluster_name or os . environ . get ( " CLUSTER_NAME " ) or first_group_data [ " cluster " ]
# Now that we have the actual cluster name, taking environment variables and defaults into account,
# we can decide what we want as the default group.
for group_data in MangoConstants [ " groups " ] :
if group_data [ " cluster " ] == actual_cluster :
default_group_data = group_data
break
2021-09-16 07:36:52 -07:00
actual_commitment : str = commitment or " processed "
2021-09-15 11:27:07 -07:00
actual_encoding : str = encoding or " base64 "
2021-12-01 08:14:30 -08:00
actual_blockhash_cache_duration : int = blockhash_cache_duration or 0
2021-12-04 05:44:22 -08:00
actual_stale_data_pauses_before_retry : typing . Sequence [ float ] = stale_data_pauses_before_retry or [ ]
2021-09-07 13:44:48 -07:00
2021-12-05 08:38:49 -08:00
actual_cluster_urls : typing . Optional [ typing . Sequence [ str ] ] = cluster_urls
if actual_cluster_urls is None or len ( actual_cluster_urls ) == 0 :
cluster_url_from_environment : typing . Optional [ str ] = os . environ . get ( " CLUSTER_URL " )
if cluster_url_from_environment is not None and cluster_url_from_environment != " " :
actual_cluster_urls = [ cluster_url_from_environment ]
else :
actual_cluster_urls = [ MangoConstants [ " cluster_urls " ] [ actual_cluster ] ]
2021-08-25 09:06:00 -07:00
actual_skip_preflight : bool = skip_preflight
actual_group_name : str = group_name or os . environ . get ( " GROUP_NAME " ) or default_group_data [ " name " ]
found_group_data : typing . Any = None
for group in MangoConstants [ " groups " ] :
if group [ " cluster " ] == actual_cluster and group [ " name " ] . upper ( ) == actual_group_name . upper ( ) :
found_group_data = group
if found_group_data is None :
2021-08-26 05:43:23 -07:00
raise Exception ( f " Could not find group named ' { actual_group_name } ' in cluster ' { actual_cluster } ' . " )
2021-08-25 09:06:00 -07:00
2021-12-05 08:38:49 -08:00
actual_group_address : PublicKey = group_address or __public_key_or_none ( os . environ . get (
2021-08-25 09:06:00 -07:00
" GROUP_ADDRESS " ) ) or PublicKey ( found_group_data [ " publicKey " ] )
2021-12-05 08:38:49 -08:00
actual_program_address : PublicKey = program_address or __public_key_or_none ( os . environ . get (
2021-08-26 02:31:02 -07:00
" MANGO_PROGRAM_ADDRESS " ) ) or PublicKey ( found_group_data [ " mangoProgramId " ] )
2021-12-05 08:38:49 -08:00
actual_serum_program_address : PublicKey = serum_program_address or __public_key_or_none ( os . environ . get (
2021-08-25 09:06:00 -07:00
" SERUM_PROGRAM_ADDRESS " ) ) or PublicKey ( found_group_data [ " serumProgramId " ] )
2021-09-13 04:17:19 -07:00
actual_gma_chunk_size : Decimal = gma_chunk_size or Decimal ( 100 )
actual_gma_chunk_pause : Decimal = gma_chunk_pause or Decimal ( 0 )
2021-11-08 03:39:09 -08:00
ids_json_token_lookup : InstrumentLookup = IdsJsonTokenLookup ( actual_cluster , actual_group_name )
instrument_lookup : InstrumentLookup = ids_json_token_lookup
2021-12-15 08:03:31 -08:00
mainnet_overrides_filename = os . path . join ( DATA_PATH , " overrides.tokenlist.json " )
devnet_overrides_filename = os . path . join ( DATA_PATH , " overrides.tokenlist.devnet.json " )
2021-08-25 09:06:00 -07:00
if actual_cluster == " mainnet " :
2021-12-15 08:03:31 -08:00
# 'Overrides' are for problematic situations.
#
# We want to be able to use the community-owned SPL Token Registry JSON file. It holds details
# of most tokens and allows our Serum code to work with any of them and their markets.
#
# The problems come when they decide to rename symbols, like they did with "ETH".
#
# Mango uses "ETH" with a mint 2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk and for a long time
# the SPL JSON file also used "ETH" with a mint of 2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk.
#
# Then the SPL JSON file was updated and the mint 2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk now
# has the symbol 'soETH' for 'Wrapped Ethereum (Sollet)', and "ETH" is now 'Wrapped Ether (Wormhole)'
# with a mint of 7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs.
#
# 'Overrides' allows us to put the details we expect for 'ETH' into our loader, ahead of the SPL
# JSON, so that our code and users can continue to use, for example, ETH/USDT, as they expect.
mainnet_overrides_token_lookup : InstrumentLookup = SPLTokenLookup . load ( mainnet_overrides_filename )
2021-11-08 03:39:09 -08:00
mainnet_spl_token_lookup : InstrumentLookup = SPLTokenLookup . load ( token_filename )
mainnet_non_spl_instrument_lookup : InstrumentLookup = NonSPLInstrumentLookup . load (
NonSPLInstrumentLookup . DefaultMainnetDataFilepath )
2021-12-15 08:03:31 -08:00
instrument_lookup = CompoundInstrumentLookup ( [
ids_json_token_lookup ,
mainnet_overrides_token_lookup ,
2021-12-15 10:15:26 -08:00
mainnet_non_spl_instrument_lookup ,
mainnet_spl_token_lookup ] )
2021-08-25 09:06:00 -07:00
elif actual_cluster == " devnet " :
2021-12-15 08:03:31 -08:00
devnet_overrides_token_lookup : InstrumentLookup = SPLTokenLookup . load ( devnet_overrides_filename )
2021-07-23 02:20:44 -07:00
devnet_token_filename = token_filename . rsplit ( ' . ' , 1 ) [ 0 ] + " .devnet.json "
2021-11-08 03:39:09 -08:00
devnet_spl_token_lookup : InstrumentLookup = SPLTokenLookup . load ( devnet_token_filename )
devnet_non_spl_instrument_lookup : InstrumentLookup = NonSPLInstrumentLookup . load (
NonSPLInstrumentLookup . DefaultDevnetDataFilepath )
2021-12-15 08:03:31 -08:00
instrument_lookup = CompoundInstrumentLookup ( [
ids_json_token_lookup ,
devnet_overrides_token_lookup ,
2021-12-15 10:15:26 -08:00
devnet_non_spl_instrument_lookup ,
devnet_spl_token_lookup ] )
2021-07-23 02:20:44 -07:00
2021-11-08 03:39:09 -08:00
ids_json_market_lookup : MarketLookup = IdsJsonMarketLookup ( actual_cluster , instrument_lookup )
2021-07-23 02:20:44 -07:00
all_market_lookup = ids_json_market_lookup
2021-08-25 09:06:00 -07:00
if actual_cluster == " mainnet " :
2021-12-15 08:03:31 -08:00
mainnet_overrides_serum_market_lookup : SerumMarketLookup = SerumMarketLookup . load (
actual_serum_program_address , mainnet_overrides_filename )
2021-08-25 09:06:00 -07:00
mainnet_serum_market_lookup : SerumMarketLookup = SerumMarketLookup . load (
actual_serum_program_address , token_filename )
2021-12-15 08:03:31 -08:00
all_market_lookup = CompoundMarketLookup ( [
ids_json_market_lookup ,
mainnet_overrides_serum_market_lookup ,
mainnet_serum_market_lookup ] )
2021-08-25 09:06:00 -07:00
elif actual_cluster == " devnet " :
2021-12-15 08:03:31 -08:00
devnet_overrides_serum_market_lookup : SerumMarketLookup = SerumMarketLookup . load (
actual_serum_program_address , devnet_overrides_filename )
2021-07-23 02:20:44 -07:00
devnet_token_filename = token_filename . rsplit ( ' . ' , 1 ) [ 0 ] + " .devnet.json "
2021-08-19 03:00:39 -07:00
devnet_serum_market_lookup : SerumMarketLookup = SerumMarketLookup . load (
2021-08-25 09:06:00 -07:00
actual_serum_program_address , devnet_token_filename )
2021-12-15 08:03:31 -08:00
all_market_lookup = CompoundMarketLookup ( [
ids_json_market_lookup ,
devnet_overrides_serum_market_lookup ,
devnet_serum_market_lookup ] )
2021-07-23 02:20:44 -07:00
market_lookup : MarketLookup = all_market_lookup
2021-12-05 08:38:49 -08:00
return Context ( actual_name , actual_cluster , actual_cluster_urls , actual_skip_preflight , actual_commitment , actual_encoding , actual_blockhash_cache_duration , actual_stale_data_pauses_before_retry , actual_program_address , actual_serum_program_address , actual_group_name , actual_group_address , actual_gma_chunk_size , actual_gma_chunk_pause , instrument_lookup , market_lookup )