2021-06-07 07:10:18 -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)
2021-06-07 11:38:43 -07:00
import argparse
2021-06-07 07:10:18 -07:00
import logging
2021-06-10 13:28:29 -07:00
import multiprocessing
2021-06-07 07:10:18 -07:00
import os
import random
import time
import typing
from decimal import Decimal
2021-06-10 13:28:29 -07:00
from rx . scheduler import ThreadPoolScheduler
2021-06-07 07:10:18 -07:00
from solana . publickey import PublicKey
from solana . rpc . api import Client
2021-06-08 16:47:15 -07:00
from solana . rpc . commitment import Commitment
2021-06-24 03:59:57 -07:00
from solana . rpc . types import MemcmpOpts , RPCError , RPCResponse , TxOpts
2021-06-07 07:10:18 -07:00
from . constants import MangoConstants , SOL_DECIMAL_DIVISOR
2021-06-25 03:10:41 -07:00
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
2021-06-07 07:10:18 -07:00
# # 🥭 Context
#
# ## 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)
#
2021-06-30 06:08:37 -07:00
_default_group_data = MangoConstants [ " groups " ] [ 0 ]
2021-06-25 02:33:40 -07:00
default_cluster = os . environ . get ( " CLUSTER " ) or _default_group_data [ " cluster " ]
2021-06-07 11:38:43 -07:00
default_cluster_url = os . environ . get ( " CLUSTER_URL " ) or MangoConstants [ " cluster_urls " ] [ default_cluster ]
2021-06-25 02:33:40 -07:00
default_program_id = PublicKey ( _default_group_data [ " mangoProgramId " ] )
default_dex_program_id = PublicKey ( _default_group_data [ " serumProgramId " ] )
2021-06-07 11:38:43 -07:00
2021-06-25 02:33:40 -07:00
default_group_name = os . environ . get ( " GROUP_NAME " ) or _default_group_data [ " name " ]
default_group_id = PublicKey ( _default_group_data [ " publicKey " ] )
2021-06-08 08:27:04 -07:00
2021-06-10 13:28:29 -07:00
# Probably best to access this through the Context object
_pool_scheduler = ThreadPoolScheduler ( multiprocessing . cpu_count ( ) )
2021-06-08 08:27:04 -07:00
2021-06-07 11:38:43 -07:00
# # 🥭 Context class
#
# A `Context` object to manage Solana connection and Mango configuration.
#
2021-06-10 13:28:29 -07:00
2021-06-07 07:10:18 -07:00
class Context :
def __init__ ( self , cluster : str , cluster_url : str , program_id : PublicKey , dex_program_id : PublicKey ,
2021-06-25 02:33:40 -07:00
group_name : str , group_id : PublicKey , token_filename : str = SplTokenLookup . DefaultDataFilepath ) :
2021-06-07 07:10:18 -07:00
self . logger : logging . Logger = logging . getLogger ( self . __class__ . __name__ )
self . cluster : str = cluster
self . cluster_url : str = cluster_url
self . client : Client = Client ( cluster_url )
2021-06-25 02:33:40 -07:00
self . program_id : PublicKey = program_id
2021-06-07 07:10:18 -07:00
self . dex_program_id : PublicKey = dex_program_id
self . group_name : str = group_name
self . group_id : PublicKey = group_id
2021-06-08 16:47:15 -07:00
self . commitment : Commitment = Commitment ( " processed " )
2021-06-24 03:59:57 -07:00
self . transaction_options : TxOpts = TxOpts ( preflight_commitment = self . commitment )
2021-06-07 07:10:18 -07:00
self . encoding : str = " base64 "
2021-06-25 07:50:37 -07:00
ids_json_token_lookup : TokenLookup = IdsJsonTokenLookup ( cluster , group_name )
2021-06-30 06:08:37 -07:00
all_token_lookup = ids_json_token_lookup
if cluster == " mainnet-beta " :
spl_token_lookup : TokenLookup = SplTokenLookup . load ( token_filename )
all_token_lookup = CompoundTokenLookup ( [ ids_json_token_lookup , spl_token_lookup ] )
2021-06-25 02:33:40 -07:00
self . token_lookup : TokenLookup = all_token_lookup
2021-06-18 05:50:33 -07:00
2021-06-25 03:10:41 -07:00
ids_json_market_lookup : MarketLookup = IdsJsonMarketLookup ( cluster )
2021-06-30 06:08:37 -07:00
all_market_lookup = ids_json_market_lookup
if cluster == " mainnet-beta " :
serum_market_lookup : SerumMarketLookup = SerumMarketLookup . load ( token_filename )
all_market_lookup = CompoundMarketLookup ( [ ids_json_market_lookup , serum_market_lookup ] )
2021-06-18 05:50:33 -07:00
self . market_lookup : MarketLookup = all_market_lookup
2021-06-07 07:10:18 -07:00
# kangda said in Discord: https://discord.com/channels/791995070613159966/836239696467591186/847816026245693451
# "I think you are better off doing 4,8,16,20,30"
2021-06-25 07:50:37 -07:00
self . retry_pauses : typing . Sequence [ Decimal ] = [ Decimal ( 4 ) , Decimal (
2021-06-07 07:10:18 -07:00
8 ) , Decimal ( 16 ) , Decimal ( 20 ) , Decimal ( 30 ) ]
2021-06-10 13:28:29 -07:00
@property
def pool_scheduler ( self ) - > ThreadPoolScheduler :
return _pool_scheduler
2021-06-17 03:18:35 -07:00
@staticmethod
def default ( ) :
return Context ( default_cluster , default_cluster_url , default_program_id ,
default_dex_program_id , default_group_name , default_group_id )
2021-06-07 07:10:18 -07:00
def fetch_sol_balance ( self , account_public_key : PublicKey ) - > Decimal :
result = self . client . get_balance ( account_public_key , commitment = self . commitment )
value = Decimal ( result [ " result " ] [ " value " ] )
return value / SOL_DECIMAL_DIVISOR
def fetch_program_accounts_for_owner ( self , program_id : PublicKey , owner : PublicKey ) :
memcmp_opts = [
MemcmpOpts ( offset = 40 , bytes = str ( owner ) ) ,
]
return self . client . get_program_accounts ( program_id , memcmp_opts = memcmp_opts , commitment = self . commitment , encoding = self . encoding )
def unwrap_or_raise_exception ( self , response : RPCResponse ) - > typing . Any :
if " error " in response :
if response [ " error " ] is str :
message : str = typing . cast ( str , response [ " error " ] )
code : int = - 1
else :
error : RPCError = typing . cast ( RPCError , response [ " error " ] )
message = error [ " message " ]
code = error [ " code " ]
raise Exception (
f " Error response from server: ' { message } ' , code: { code } " )
return response [ " result " ]
def unwrap_transaction_id_or_raise_exception ( self , response : RPCResponse ) - > str :
return typing . cast ( str , self . unwrap_or_raise_exception ( response ) )
def random_client_id ( self ) - > int :
# 9223372036854775807 is sys.maxsize for 64-bit systems, with a bit_length of 63.
# We explicitly want to use a max of 64-bits though, so we use the number instead of
# sys.maxsize, which could be lower on 32-bit systems or higher on 128-bit systems.
return random . randrange ( 9223372036854775807 )
def lookup_group_name ( self , group_address : PublicKey ) - > str :
2021-06-28 01:48:16 -07:00
group_address_str = str ( group_address )
for group in MangoConstants [ " groups " ] :
if group [ " cluster " ] == self . cluster and group [ " publicKey " ] == group_address_str :
return group [ " name " ]
2021-06-07 07:10:18 -07:00
2021-06-28 01:48:16 -07:00
return " « Unknown Group » "
2021-06-07 07:10:18 -07:00
def wait_for_confirmation ( self , transaction_id : str , max_wait_in_seconds : int = 60 ) - > typing . Optional [ typing . Dict ] :
self . logger . info (
f " Waiting up to { max_wait_in_seconds } seconds for { transaction_id } . " )
for wait in range ( 0 , max_wait_in_seconds ) :
time . sleep ( 1 )
2021-06-17 03:18:35 -07:00
confirmed = self . client . get_confirmed_transaction ( transaction_id )
2021-06-07 07:10:18 -07:00
if confirmed [ " result " ] is not None :
self . logger . info ( f " Confirmed after { wait } seconds. " )
return confirmed [ " result " ]
self . logger . info ( f " Timed out after { wait } seconds waiting on transaction { transaction_id } . " )
return None
def new_from_cluster ( self , cluster : str ) - > " Context " :
cluster_url = MangoConstants [ " cluster_urls " ] [ cluster ]
2021-06-25 02:33:40 -07:00
for group_data in MangoConstants [ " groups " ] :
if group_data [ " cluster " ] == cluster :
if group_data [ " name " ] == self . group_name :
program_id = PublicKey ( group_data [ " mangoProgramId " ] )
dex_program_id = PublicKey ( group_data [ " serumProgramId " ] )
group_id = PublicKey ( _default_group_data [ " publicKey " ] )
return Context ( cluster , cluster_url , program_id , dex_program_id , self . group_name , group_id )
raise Exception ( f " Could not find group name ' { self . group_name } ' in cluster ' { cluster } ' . " )
2021-06-07 07:10:18 -07:00
def new_from_cluster_url ( self , cluster_url : str ) - > " Context " :
return Context ( self . cluster , cluster_url , self . program_id , self . dex_program_id , self . group_name , self . group_id )
def new_from_group_name ( self , group_name : str ) - > " Context " :
2021-06-25 02:33:40 -07:00
for group_data in MangoConstants [ " groups " ] :
if group_data [ " cluster " ] == self . cluster :
if group_data [ " name " ] == group_name :
program_id = PublicKey ( group_data [ " mangoProgramId " ] )
dex_program_id = PublicKey ( group_data [ " serumProgramId " ] )
group_id = PublicKey ( _default_group_data [ " publicKey " ] )
return Context ( self . cluster , self . cluster_url , program_id , dex_program_id , group_name , group_id )
raise Exception ( f " Could not find group name ' { group_name } ' in cluster ' { self . cluster } ' . " )
2021-06-07 07:10:18 -07:00
def new_from_group_id ( self , group_id : PublicKey ) - > " Context " :
group_id_str = str ( group_id )
2021-06-25 02:33:40 -07:00
for group_data in MangoConstants [ " groups " ] :
if group_data [ " cluster " ] == self . cluster :
if group_data [ " publicKey " ] == group_id_str :
program_id = PublicKey ( group_data [ " mangoProgramId " ] )
dex_program_id = PublicKey ( group_data [ " serumProgramId " ] )
group_id = PublicKey ( _default_group_data [ " publicKey " ] )
group_name = _default_group_data [ " name " ]
return Context ( self . cluster , self . cluster_url , program_id , dex_program_id , group_name , group_id )
raise Exception ( f " Could not find group with ID ' { group_id } ' in cluster ' { self . cluster } ' . " )
2021-06-07 07:10:18 -07:00
@staticmethod
def from_cluster_and_group_name ( cluster : str , group_name : str ) - > " Context " :
cluster_url = MangoConstants [ " cluster_urls " ] [ cluster ]
program_id = PublicKey ( MangoConstants [ cluster ] [ " mango_program_id " ] )
dex_program_id = PublicKey ( MangoConstants [ cluster ] [ " dex_program_id " ] )
group_id = PublicKey ( MangoConstants [ cluster ] [ " mango_groups " ] [ group_name ] [ " mango_group_pk " ] )
return Context ( cluster , cluster_url , program_id , dex_program_id , group_name , group_id )
2021-06-07 11:38:43 -07:00
# 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-06-26 10:24:07 -07:00
def add_command_line_parameters ( parser : argparse . ArgumentParser , logging_default = logging . INFO ) - > None :
2021-06-07 11:38:43 -07:00
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 ( " --program-id " , type = str , default = default_program_id ,
help = " Mango program ID/address " )
parser . add_argument ( " --dex-program-id " , type = str , 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 = str , default = default_group_id ,
help = " Mango group ID/address " )
2021-06-25 02:33:40 -07:00
parser . add_argument ( " --token-data-file " , type = str , default = SplTokenLookup . DefaultDataFilepath ,
2021-06-17 04:06:39 -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 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.
2021-06-26 10:24:07 -07:00
parser . add_argument ( " --log-level " , default = logging_default , type = lambda level : getattr ( logging , level ) ,
2021-06-17 04:06:39 -07:00
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
2021-06-07 11:38:43 -07:00
# an argument of parsed command-line parameters and expects to see the ones it added
2021-06-17 04:06:39 -07:00
# to that collection in the `add_command_line_parameters()` call.
2021-06-07 11:38:43 -07:00
#
# It then uses those parameters to create a properly-configured `Context` object.
#
@staticmethod
2021-06-17 04:06:39 -07:00
def from_command_line_parameters ( args : argparse . Namespace ) - > " Context " :
2021-06-07 11:38:43 -07:00
# 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.
group_id = args . group_id
if ( args . group_name != default_group_name ) and ( group_id == default_group_id ) :
2021-06-25 02:33:40 -07:00
for group in MangoConstants [ " groups " ] :
if group [ " cluster " ] == args . cluster and group [ " name " ] . upper ( ) == args . group_name . upper ( ) :
group_id = PublicKey ( group [ " publicKey " ] )
elif ( args . cluster != default_cluster ) and ( group_id == default_group_id ) :
for group in MangoConstants [ " groups " ] :
if group [ " cluster " ] == args . cluster and group [ " name " ] . upper ( ) == args . group_name . upper ( ) :
group_id = PublicKey ( group [ " publicKey " ] )
2021-06-07 11:38:43 -07:00
2021-06-16 12:38:00 -07:00
# 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.
cluster_url = args . cluster_url
if ( args . cluster != default_cluster ) and ( cluster_url == default_cluster_url ) :
cluster_url = MangoConstants [ " cluster_urls " ] [ args . cluster ]
2021-06-07 11:38:43 -07:00
program_id = args . program_id
if group_id == PublicKey ( " 7pVYhpKUHw88neQHxgExSH6cerMZ1Axx1ALQP9sxtvQV " ) :
program_id = PublicKey ( " JD3bq9hGdy38PuWQ4h2YJpELmHVGPPfFSuFkpzAd9zfu " )
2021-06-16 12:38:00 -07:00
return Context ( args . cluster , cluster_url , program_id , args . dex_program_id , args . group_name , group_id )
2021-06-07 11:38:43 -07:00
2021-06-07 07:10:18 -07:00
def __str__ ( self ) - > str :
2021-06-10 13:28:29 -07:00
return f """ « 𝙲 𝚘 𝚗 𝚝 𝚎 𝚡 𝚝 :
2021-06-07 07:10:18 -07:00
Cluster : { self . cluster }
Cluster URL : { self . cluster_url }
Program ID : { self . program_id }
DEX Program ID : { self . dex_program_id }
Group Name : { self . group_name }
Group ID : { self . group_id }
» """
def __repr__ ( self ) - > str :
return f " { self } "