strideutils 0.2.11__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {strideutils-0.2.11 → strideutils-0.3.0}/PKG-INFO +1 -1
  2. {strideutils-0.2.11 → strideutils-0.3.0}/pyproject.toml +1 -1
  3. strideutils-0.3.0/strideutils/coingecko.py +147 -0
  4. {strideutils-0.2.11 → strideutils-0.3.0}/strideutils/config_examples/strideutils_config.yaml.example +24 -12
  5. {strideutils-0.2.11 → strideutils-0.3.0}/strideutils/invariant_helpers.py +1 -1
  6. {strideutils-0.2.11 → strideutils-0.3.0}/strideutils/run_safely.py +1 -0
  7. strideutils-0.3.0/strideutils/stride_config.py +367 -0
  8. {strideutils-0.2.11 → strideutils-0.3.0}/strideutils/stride_requests.py +37 -97
  9. strideutils-0.2.11/strideutils/coingecko.py +0 -35
  10. strideutils-0.2.11/strideutils/stride_config.py +0 -301
  11. {strideutils-0.2.11 → strideutils-0.3.0}/README.md +0 -0
  12. {strideutils-0.2.11 → strideutils-0.3.0}/strideutils/Makefile +0 -0
  13. {strideutils-0.2.11 → strideutils-0.3.0}/strideutils/__init__.py +0 -0
  14. {strideutils-0.2.11 → strideutils-0.3.0}/strideutils/config_examples/strideutils_secrets.yaml.example +0 -0
  15. {strideutils-0.2.11 → strideutils-0.3.0}/strideutils/cryptography.py +0 -0
  16. {strideutils-0.2.11 → strideutils-0.3.0}/strideutils/exec_tx.py +0 -0
  17. {strideutils-0.2.11 → strideutils-0.3.0}/strideutils/sheets_connector.py +0 -0
  18. {strideutils-0.2.11 → strideutils-0.3.0}/strideutils/slack_connector.py +0 -0
  19. {strideutils-0.2.11 → strideutils-0.3.0}/strideutils/stride_alerts.py +0 -0
  20. {strideutils-0.2.11 → strideutils-0.3.0}/strideutils/stride_upstash.py +0 -0
  21. {strideutils-0.2.11 → strideutils-0.3.0}/strideutils/twilio_connector.py +0 -0
  22. {strideutils-0.2.11 → strideutils-0.3.0}/strideutils-stubs/__init__.pyi +0 -0
  23. {strideutils-0.2.11 → strideutils-0.3.0}/strideutils-stubs/coingecko.pyi +0 -0
  24. {strideutils-0.2.11 → strideutils-0.3.0}/strideutils-stubs/cryptography.pyi +0 -0
  25. {strideutils-0.2.11 → strideutils-0.3.0}/strideutils-stubs/exec_tx.pyi +0 -0
  26. {strideutils-0.2.11 → strideutils-0.3.0}/strideutils-stubs/invariant_helpers.pyi +0 -0
  27. {strideutils-0.2.11 → strideutils-0.3.0}/strideutils-stubs/run_safely.pyi +0 -0
  28. {strideutils-0.2.11 → strideutils-0.3.0}/strideutils-stubs/sheets_connector.pyi +0 -0
  29. {strideutils-0.2.11 → strideutils-0.3.0}/strideutils-stubs/slack_connector.pyi +0 -0
  30. {strideutils-0.2.11 → strideutils-0.3.0}/strideutils-stubs/stride_alerts.pyi +0 -0
  31. {strideutils-0.2.11 → strideutils-0.3.0}/strideutils-stubs/stride_config.pyi +0 -0
  32. {strideutils-0.2.11 → strideutils-0.3.0}/strideutils-stubs/stride_requests.pyi +0 -0
  33. {strideutils-0.2.11 → strideutils-0.3.0}/strideutils-stubs/stride_upstash.pyi +0 -0
  34. {strideutils-0.2.11 → strideutils-0.3.0}/strideutils-stubs/twilio_connector.pyi +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: strideutils
3
- Version: 0.2.11
3
+ Version: 0.3.0
4
4
  Summary:
5
5
  Author: Your Name
6
6
  Author-email: you@example.com
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "strideutils"
3
- version = "0.2.11"
3
+ version = "0.3.0"
4
4
  description = ""
5
5
  authors = ["Your Name <you@example.com>"]
6
6
  readme = "README.md"
@@ -0,0 +1,147 @@
1
+ """
2
+ Exposes an easy API to get prices from coingecko
3
+ """
4
+
5
+ from typing import List
6
+
7
+ import pandas as pd
8
+ import requests
9
+
10
+ from strideutils import stride_config, stride_requests
11
+ from strideutils.stride_config import config
12
+
13
+ COINGECKO_ENDPOINT = "https://pro-api.coingecko.com/"
14
+ COINGECKO_PRICE_QUERY = "api/v3/simple/price?ids={ticker_id}&vs_currencies=usd"
15
+
16
+
17
+ def get_coingecko_name(chain_config: stride_config.ChainConfig) -> str:
18
+ """
19
+ Returns the coingecko name for a given chain
20
+ """
21
+ try:
22
+ return chain_config.coingecko_name
23
+ except AttributeError:
24
+ return chain_config.name
25
+
26
+
27
+ def get_token_price(
28
+ ticker: str,
29
+ api_token: str = config.COINGECKO_API_TOKEN,
30
+ cache_response: bool = False,
31
+ ):
32
+ """
33
+ Reads token price from coingecko
34
+ If a redis_client is provided, then store the value.
35
+ """
36
+ # TODO: Consider using the coingecko ID for stTokens instead of the redemption rate
37
+ # Get redemption rate for calculating st token prices
38
+ redemption_rate = float(1)
39
+ if ticker.startswith('st') and ticker[3].isupper():
40
+ redemption_rate = stride_requests.get_redemption_rate(ticker[2:])
41
+ ticker = ticker[2:]
42
+ # get coingecko_name
43
+ try:
44
+ coingecko_name = get_coingecko_name(config.get_chain(ticker=ticker))
45
+ except KeyError:
46
+ coingecko_name = ticker
47
+ # query endopint
48
+ endpoint = COINGECKO_ENDPOINT + COINGECKO_PRICE_QUERY.format(ticker_id=coingecko_name)
49
+ headers = {'x-cg-pro-api-key': api_token}
50
+ response = stride_requests.request(endpoint, headers=headers, cache_response=cache_response)
51
+ price = response[coingecko_name]["usd"] * redemption_rate
52
+
53
+ return price
54
+
55
+
56
+ def get_token_price_history(token: str, num_days=90, use_key=False, memo={}) -> pd.Series:
57
+ """
58
+ returns a Pandas Series with token price data going back num_days
59
+ e.g. get_token_price_history('uatom', 30) returns a Series with 30 days of ATOM price data
60
+ TODO: Figure out why API key is throwing an error when used.
61
+ """
62
+ price_key = (token, num_days)
63
+ if price_key not in memo:
64
+ tstr = get_coingecko_name(config.get_chain(ticker=token))
65
+ rurl = f"https://api.coingecko.com/api/v3/coins/{tstr}/market_chart?vs_currency=usd&days={num_days}"
66
+ rel_headers = {"x-cg-pro-api-key": config.COINGECKO_API_TOKEN}
67
+ if not use_key:
68
+ rel_headers = None
69
+ r = requests.get(
70
+ rurl,
71
+ headers=rel_headers,
72
+ )
73
+ assert r.status_code == 200
74
+ out_df = pd.DataFrame(r.json()["prices"])
75
+ out_df.columns = ["time", "price"]
76
+ out_df["time"] = pd.to_datetime(out_df["time"], unit="ms")
77
+ out_df["price"] = out_df["price"].astype(float)
78
+ out_df.set_index("time", inplace=True)
79
+ memo[token] = out_df
80
+ return memo[token]
81
+
82
+
83
+ def get_token_volume_history(token: str, num_days=90, use_key=False, memo={}) -> pd.Series:
84
+ """
85
+ returns a Pandas Series with token volume data going back num_days
86
+ e.g. get_token_volume_history('uatom', 30) returns a Series with 30 days of ATOM volume data
87
+ TODO: Figure out why API key is throwing an error when used.
88
+ """
89
+ volume_key = (token, num_days)
90
+ if volume_key not in memo:
91
+ tstr = get_coingecko_name(config.get_chain(ticker=token))
92
+ rurl = f"https://api.coingecko.com/api/v3/coins/{tstr}/market_chart?vs_currency=usd&days={num_days}"
93
+ rel_headers = {"x-cg-pro-api-key": config.COINGECKO_API_TOKEN}
94
+ if not use_key:
95
+ rel_headers = None
96
+ r = requests.get(
97
+ rurl,
98
+ headers=rel_headers,
99
+ )
100
+ assert r.status_code == 200
101
+ out_df = pd.DataFrame(r.json()["total_volumes"])
102
+ out_df.columns = ["time", "volume"]
103
+ out_df["time"] = pd.to_datetime(out_df["time"], unit="ms")
104
+ out_df["volume"] = out_df["volume"].astype(float)
105
+ out_df.set_index("time", inplace=True)
106
+ memo[token] = out_df
107
+ return memo[token]
108
+
109
+
110
+ def get_dataframe_of_prices(token_list: List[str], num_days: int = 90) -> pd.DataFrame:
111
+ """
112
+ returns a Pandas DataFrame with token price data going back num_days, for the given tokens
113
+ e.g. get_dataframe_of_prices(["ATOM", "OSMO", "STRD"], 30) returns a DataFrame of 30 days of price data
114
+ """
115
+ # gather the dataframe
116
+ df = {}
117
+ for token in token_list:
118
+ df[token] = get_token_price_history(token, num_days)["price"]
119
+ df = pd.DataFrame(df)
120
+ # resample based on how much data we're gathering
121
+ if num_days > 90:
122
+ df = df.resample("1D").last()
123
+ elif num_days >= 2:
124
+ df = df.resample("1H").last()
125
+ else:
126
+ df = df.resample("5min").last()
127
+ return df
128
+
129
+
130
+ def get_dataframe_of_volumes(token_list: List[str], num_days: int = 90) -> pd.DataFrame:
131
+ """
132
+ returns a Pandas DataFrame with token volume data going back num_days, for the given tokens
133
+ e.g. get_dataframe_of_volumes(["ATOM", "OSMO", "STRD"], 30) returns a DataFrame of 30 days of volume data
134
+ """
135
+ # gather the dataframe
136
+ df = {}
137
+ for token in token_list:
138
+ df[token] = get_token_volume_history(token, num_days)["volume"]
139
+ df = pd.DataFrame(df)
140
+ # resample based on how much data we're gathering
141
+ if num_days > 90:
142
+ df = df.resample("1D").last()
143
+ elif num_days >= 2:
144
+ df = df.resample("1H").last()
145
+ else:
146
+ df = df.resample("5min").last()
147
+ return df
@@ -25,8 +25,8 @@ stride:
25
25
  denom_decimals: 6
26
26
  api_endpoint:
27
27
  rpc_endpoint:
28
- library_api:
29
- library_rpc:
28
+ library_api_endpoint:
29
+ library_rpc_endpoint:
30
30
 
31
31
  # Host Zone Chains
32
32
  host_zones:
@@ -37,8 +37,8 @@ host_zones:
37
37
  denom: uatom
38
38
  ticker: ATOM
39
39
  denom_decimals: 6
40
+ denom_on_stride: ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2
40
41
  inflation_frequency_hours: -1 # Every block
41
- stride_token_hash: ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2
42
42
  api_endpoint:
43
43
  rpc_endpoint:
44
44
 
@@ -48,8 +48,8 @@ host_zones:
48
48
  denom: uosmo
49
49
  ticker: OSMO
50
50
  denom_decimals: 6
51
+ denom_on_stride: ibc/D24B4564BCD51D3D02D9987D92571EAC5915676A9BD6D9B0C1D0254CB8A5EA34
51
52
  inflation_frequency_hours: 24
52
- stride_token_hash: ibc/D24B4564BCD51D3D02D9987D92571EAC5915676A9BD6D9B0C1D0254CB8A5EA34
53
53
  api_endpoint:
54
54
  rpc_endpoint:
55
55
 
@@ -59,8 +59,8 @@ host_zones:
59
59
  denom: aevmos
60
60
  ticker: EVMOS
61
61
  denom_decimals: 18
62
+ denom_on_stride: ibc/4B322204B4F59D770680FE4D7A565DDC3F37BFF035474B717476C66A4F83DD72
62
63
  inflation_frequency_hours: 24
63
- stride_token_hash: ibc/4B322204B4F59D770680FE4D7A565DDC3F37BFF035474B717476C66A4F83DD72
64
64
  api_endpoint:
65
65
  rpc_endpoint:
66
66
 
@@ -71,8 +71,8 @@ host_zones:
71
71
  denom: inj
72
72
  ticker: INJ
73
73
  denom_decimals: 18
74
+ denom_on_stride: ibc/A7454562FF29FE068F42F9DE4805ABEF54F599D1720B345D6518D9B5C64EA6D2
74
75
  inflation_frequency_hours: -1 # Every block
75
- stride_token_hash: ibc/A7454562FF29FE068F42F9DE4805ABEF54F599D1720B345D6518D9B5C64EA6D2
76
76
  api_endpoint:
77
77
  rpc_endpoint:
78
78
 
@@ -83,8 +83,8 @@ host_zones:
83
83
  coingecko_name: juno-network
84
84
  ticker: JUNO
85
85
  denom_decimals: 6
86
+ denom_on_stride: ibc/DA356E369C3E5CF6A9F1DCD99CE8ED55FBD595E676A5CF033CE784C060492D5A
86
87
  inflation_frequency_hours: -1 # Every block
87
- stride_token_hash: ibc/DA356E369C3E5CF6A9F1DCD99CE8ED55FBD595E676A5CF033CE784C060492D5A
88
88
  api_endpoint:
89
89
  rpc_endpoint:
90
90
 
@@ -94,8 +94,8 @@ host_zones:
94
94
  denom: ustars
95
95
  ticker: STARS
96
96
  denom_decimals: 6
97
+ denom_on_stride: ibc/7EAE5BEF3A26B64AFBD89828AFDDB1DC7024A0276D22745201632C40E6E634D0
97
98
  inflation_frequency_hours: -1 # Every block
98
- stride_token_hash: ibc/7EAE5BEF3A26B64AFBD89828AFDDB1DC7024A0276D22745201632C40E6E634D0
99
99
  api_endpoint:
100
100
  rpc_endpoint:
101
101
 
@@ -106,8 +106,8 @@ host_zones:
106
106
  denom: uluna
107
107
  ticker: LUNA
108
108
  denom_decimals: 6
109
+ denom_on_stride: ibc/E61BCB1126F42A2ED73B4CEA2221C9635BC2102F0417543C38071779F991942E
109
110
  inflation_frequency_hours: -1 # Every block
110
- stride_token_hash: ibc/E61BCB1126F42A2ED73B4CEA2221C9635BC2102F0417543C38071779F991942E
111
111
  api_endpoint:
112
112
  rpc_endpoint:
113
113
 
@@ -117,8 +117,8 @@ host_zones:
117
117
  denom: uumee
118
118
  ticker: UMEE
119
119
  denom_decimals: 6
120
+ denom_on_stride: ibc/1A2271226209D309902AFF4F21BD21237CB514DD24EA2EE0423BF74C6135D8B8
120
121
  inflation_frequency_hours: -1 # Every block
121
- stride_token_hash: ibc/1A2271226209D309902AFF4F21BD21237CB514DD24EA2EE0423BF74C6135D8B8
122
122
  api_endpoint:
123
123
  rpc_endpoint:
124
124
 
@@ -128,8 +128,8 @@ host_zones:
128
128
  denom: ucmdx
129
129
  ticker: CMDX
130
130
  denom_decimals: 6
131
+ denom_on_stride: ibc/EB66980014602E6BD50A1CB9FFB8FA694DC3EC10A48D2C1C649D732954F88D4A
131
132
  inflation_frequency_hours: -1 # Every block
132
- stride_token_hash: ibc/EB66980014602E6BD50A1CB9FFB8FA694DC3EC10A48D2C1C649D732954F88D4A
133
133
  api_endpoint:
134
134
  rpc_endpoint:
135
135
 
@@ -139,11 +139,23 @@ host_zones:
139
139
  denom: usomm
140
140
  ticker: SOMM
141
141
  denom_decimals: 6
142
+ denom_on_stride: ibc/B86EFF6D227E3E33D7E3B5E65D0C1BB5BD79CCB56D35A9D824F0DD5D52CA43BA
142
143
  inflation_frequency_hours: -1 # Every block
143
- stride_token_hash: ibc/B86EFF6D227E3E33D7E3B5E65D0C1BB5BD79CCB56D35A9D824F0DD5D52CA43BA
144
144
  api_endpoint:
145
145
  rpc_endpoint:
146
146
 
147
+ celestia:
148
+ name: celestia
149
+ id: celestia
150
+ coingecko_name: celestia
151
+ denom: utia
152
+ ticker: TIA
153
+ denom_on_stride: ibc/BF3B4F53F3694B66E13C23107C84B6485BD2B96296BB7EC680EA77BBA75B4801
154
+ inflation_frequency_hours: -1 # Every block
155
+ api_endpoint: https://celestia-strd-api.polkachu.com
156
+ rpc_endpoint: https://celestia-strd-rpc.polkachu.com
157
+ ica_enabled: false
158
+
147
159
  # Other endpoints
148
160
  app_chains:
149
161
  neutron:
@@ -16,7 +16,7 @@ def check_slack_on_mainnet(str_output: bool = True, verbose: bool = False) -> Un
16
16
  output = ""
17
17
  for zone in config.host_zones:
18
18
  host_zone_id = zone.id
19
- host_zone = stride_requests.get_host_zone_json(host_zone_id)
19
+ host_zone = stride_requests.get_host_zone(host_zone_id)
20
20
 
21
21
  rr = host_zone['redemption_rate']
22
22
  min_rr, max_rr = host_zone['min_redemption_rate'], host_zone['max_redemption_rate']
@@ -32,6 +32,7 @@ def try_and_log_with_status(
32
32
  func()
33
33
  msg = [f':white_check_mark: {func_name}']
34
34
  except Exception:
35
+ print(traceback.format_exc())
35
36
  msg = [f':x: {func_name} ']
36
37
  tracebacks.append(f"{func_name} \n\n" + traceback.format_exc())
37
38
  msgs += msg
@@ -0,0 +1,367 @@
1
+ """
2
+ This file loads and provide an object `config` to access all secrets, configs, and chain info
3
+ """
4
+
5
+ import itertools
6
+ import os
7
+ from dataclasses import dataclass, field, fields
8
+ from typing import Any, List, Optional, Union
9
+
10
+ import yaml
11
+
12
+ MISSING_CONFIG_MSG = "Configuration or environment variable '{x}' is not set or unknown"
13
+
14
+ ENV = os.environ.get("ENV")
15
+ if ENV not in ["DEV", "PROD"]:
16
+ raise ValueError("Environment variable 'ENV' must be set to either DEV or PROD")
17
+
18
+
19
+ @dataclass
20
+ class ConfigObj:
21
+ """Raise an error if a config is not set."""
22
+
23
+ def __getattribute__(self, name):
24
+ """
25
+ Called every time a field is attempted to be accessed
26
+ Falls back to getattr if the field is not found
27
+ """
28
+ value = super().__getattribute__(name)
29
+ if value == "" or value is None:
30
+ raise AttributeError(MISSING_CONFIG_MSG.format(x=name))
31
+ return value
32
+
33
+ def __iter__(self):
34
+ """Allow iterating over set values"""
35
+ for subfield in fields(self):
36
+ if hasattr(self, subfield.name):
37
+ yield getattr(self, subfield.name)
38
+
39
+ def __contains__(self, field) -> bool:
40
+ """Allows for checking if an attribute is present with `in`"""
41
+ if not hasattr(self, field):
42
+ return False
43
+ return bool(getattr(self, field))
44
+
45
+
46
+ class ConfigDict(dict):
47
+ def __getitem__(self, key):
48
+ """Raise an error if an unset key is indexed."""
49
+ if key not in self:
50
+ raise KeyError(MISSING_CONFIG_MSG.format(x=key))
51
+ value = super().__getitem__(key)
52
+ if value == "" or value is None:
53
+ raise KeyError(MISSING_CONFIG_MSG.format(x=key))
54
+ return value
55
+
56
+
57
+ # Use ConfigDict in the yaml parser
58
+ class Loader(yaml.FullLoader):
59
+ def construct_yaml_map(self, node):
60
+ data = ConfigDict()
61
+ yield data
62
+ value = self.construct_mapping(node)
63
+ data.update(value)
64
+
65
+
66
+ Loader.add_constructor('tag:yaml.org,2002:map', Loader.construct_yaml_map)
67
+
68
+
69
+ @dataclass(repr=False)
70
+ class ChainConfig(ConfigObj):
71
+ """Config relevant for all chains"""
72
+
73
+ name: str = ""
74
+ id: str = ""
75
+ coingecko_name: str = ""
76
+ denom: str = ""
77
+ denom_decimals: int = 6
78
+ ticker: str = ""
79
+ api_endpoint: str = ""
80
+ rpc_endpoint: str = ""
81
+
82
+ def __repr__(self) -> str:
83
+ return f"ChainConfig(name={self.name}, id={self.id})"
84
+
85
+
86
+ @dataclass(repr=False)
87
+ class StrideChainConfig(ChainConfig):
88
+ """Config specific to stride"""
89
+
90
+ library_api_endpoint: str = ""
91
+ library_rpc_endpoint: str = ""
92
+
93
+ def __repr__(self) -> str:
94
+ return f"StrideChainConfig(name={self.name}, id={self.id})"
95
+
96
+
97
+ @dataclass(repr=False)
98
+ class HostChainConfig(ChainConfig):
99
+ """Config specific to stride's host zones"""
100
+
101
+ # Frequency with which staking rewards are issued
102
+ # -1 indicates every block
103
+ inflation_frequency_hours: int = -1
104
+ # IBC denom of the native token as it sits on stride
105
+ denom_on_stride: str = ""
106
+ # Indicates whether the chain has ICA enabled
107
+ ica_enabled: bool = True
108
+
109
+ def __repr__(self) -> str:
110
+ return f"HostChainConfig(name={self.name}, id={self.id})"
111
+
112
+
113
+ @dataclass(repr=False)
114
+ class HostChainsConfig(ConfigObj):
115
+ cosmoshub: HostChainConfig = field(default_factory=HostChainConfig)
116
+ osmosis: HostChainConfig = field(default_factory=HostChainConfig)
117
+ evmos: HostChainConfig = field(default_factory=HostChainConfig)
118
+ injective: HostChainConfig = field(default_factory=HostChainConfig)
119
+ juno: HostChainConfig = field(default_factory=HostChainConfig)
120
+ stargaze: HostChainConfig = field(default_factory=HostChainConfig)
121
+ terra: HostChainConfig = field(default_factory=HostChainConfig)
122
+ umee: HostChainConfig = field(default_factory=HostChainConfig)
123
+ comdex: HostChainConfig = field(default_factory=HostChainConfig)
124
+ sommelier: HostChainConfig = field(default_factory=HostChainConfig)
125
+ dydx: HostChainConfig = field(default_factory=HostChainConfig)
126
+ saga: HostChainConfig = field(default_factory=HostChainConfig)
127
+ celestia: HostChainConfig = field(default_factory=HostChainConfig)
128
+ dymension: HostChainConfig = field(default_factory=HostChainConfig)
129
+
130
+
131
+ @dataclass(repr=False)
132
+ class AppChainsConfig(ConfigObj):
133
+ neutron: ChainConfig = field(default_factory=ChainConfig)
134
+ kava: ChainConfig = field(default_factory=ChainConfig)
135
+ iris: ChainConfig = field(default_factory=ChainConfig)
136
+ akash: ChainConfig = field(default_factory=ChainConfig)
137
+ sentinel: ChainConfig = field(default_factory=ChainConfig)
138
+ persistence: ChainConfig = field(default_factory=ChainConfig)
139
+ cryptoorg: ChainConfig = field(default_factory=ChainConfig)
140
+ kichain: ChainConfig = field(default_factory=ChainConfig)
141
+ bitcanna: ChainConfig = field(default_factory=ChainConfig)
142
+ regen: ChainConfig = field(default_factory=ChainConfig)
143
+ gravity: ChainConfig = field(default_factory=ChainConfig)
144
+ desmos: ChainConfig = field(default_factory=ChainConfig)
145
+ chihuahua: ChainConfig = field(default_factory=ChainConfig)
146
+ secret: ChainConfig = field(default_factory=ChainConfig)
147
+ axelar: ChainConfig = field(default_factory=ChainConfig)
148
+ crescent: ChainConfig = field(default_factory=ChainConfig)
149
+ mantle: ChainConfig = field(default_factory=ChainConfig)
150
+ mars: ChainConfig = field(default_factory=ChainConfig)
151
+ canto: ChainConfig = field(default_factory=ChainConfig)
152
+ agoric: ChainConfig = field(default_factory=ChainConfig)
153
+
154
+
155
+ @dataclass(repr=False)
156
+ class Config(ConfigObj):
157
+ ENV: str
158
+ timezone: str = "US/Eastern"
159
+
160
+ # Twilio
161
+ TWILIO_ALERTS_NUMBER: str = ""
162
+ TWILIO_ACCOUNT_ID: str = ""
163
+ TWILIO_API_TOKEN: str = ""
164
+ PHONE_NUMBERS: ConfigDict = field(default_factory=ConfigDict)
165
+
166
+ # Slack
167
+ STRIDEBOT_API_TOKEN: str = ""
168
+
169
+ # Gsheets - loads from str env var in PROD
170
+ PUBLICSHEETS_AUTH: Union[str, ConfigDict] = field(default_factory=ConfigDict)
171
+
172
+ # Coingecko
173
+ COINGECKO_API_TOKEN: str = ""
174
+
175
+ # Protocol Staking (dydx yield)
176
+ PROTOCOL_STAKING_API_TOKEN: str = ""
177
+
178
+ # Stride alerts
179
+ alerts_playbook: str = ""
180
+ slack_channels: ConfigDict = field(default_factory=ConfigDict)
181
+
182
+ # Upstash
183
+ UPSTASH_PUBLIC_PASSWORD: str = ""
184
+ UPSTASH_STRIDE_FRONTEND_PASSWORD: str = ""
185
+ UPSTASH_STRIDE_BACKEND_PASSWORD: str = ""
186
+ UPSTASH_STRIDE_DYDX_PUBLIC_PASSWORD: str = ""
187
+
188
+ # Stride internal secrets
189
+ SLACK_BEARER_TOKEN: str = ""
190
+ SLACK_SUCCESS_CHANNEL_ID: str = ""
191
+ SLACK_INVARIANT_FAILURE_CHANNEL_ID: str = ""
192
+ SLACK_PACKET_FAILURE_CHANNEL_ID: str = ""
193
+ NUMIA_API_TOKEN: str = ""
194
+ founders: List[str] = field(default_factory=lambda: ['riley', 'aidan', 'vishal'])
195
+
196
+ # Chain configs
197
+ stride: StrideChainConfig = field(default_factory=StrideChainConfig)
198
+ host_zones: HostChainsConfig = field(default_factory=HostChainsConfig)
199
+ app_chains: AppChainsConfig = field(default_factory=AppChainsConfig)
200
+
201
+ @property
202
+ def stakeibc_host_zones(self) -> List[HostChainConfig]:
203
+ return [host_zone for host_zone in self.host_zones if host_zone.ica_enabled]
204
+
205
+ def _create_mappings(self) -> None:
206
+ """
207
+ Create a reverse map from attributes to ChainConfig if id, ticker, and denom are set.
208
+ """
209
+ name_to_zone = ConfigDict()
210
+ id_to_zone = ConfigDict()
211
+ ticker_to_zone = ConfigDict()
212
+ denom_to_zone = ConfigDict()
213
+ denom_on_stride_to_zone = ConfigDict()
214
+
215
+ # Map host zones
216
+ for chain_info in itertools.chain(self.host_zones, self.app_chains):
217
+ try:
218
+ name_to_zone[chain_info.name] = chain_info
219
+ except AttributeError:
220
+ pass
221
+
222
+ try:
223
+ id_to_zone[chain_info.id] = chain_info
224
+ except AttributeError:
225
+ pass
226
+
227
+ try:
228
+ ticker_to_zone[chain_info.ticker] = chain_info
229
+ ticker_to_zone[chain_info.ticker.upper()] = chain_info
230
+ ticker_to_zone[chain_info.ticker.lower()] = chain_info
231
+ ticker_to_zone[chain_info.ticker.denom[1].upper()] = chain_info
232
+ except AttributeError:
233
+ pass
234
+
235
+ try:
236
+ denom_to_zone[chain_info.denom] = chain_info
237
+ except AttributeError:
238
+ pass
239
+
240
+ try:
241
+ denom_on_stride_to_zone[chain_info.denom_on_stride] = chain_info
242
+ except AttributeError:
243
+ pass
244
+
245
+ self._name_to_zone = name_to_zone
246
+ self._id_to_zone = id_to_zone
247
+ self._ticker_to_zone = ticker_to_zone
248
+ self._denom_to_zone = denom_to_zone
249
+ self._denom_on_stride_to_zone = denom_on_stride_to_zone
250
+
251
+ def __post_init__(self):
252
+ self._create_mappings()
253
+
254
+ def get_chain(
255
+ self,
256
+ name: Optional[str] = None,
257
+ id: Optional[str] = None,
258
+ ticker: Optional[str] = None,
259
+ denom: Optional[str] = None,
260
+ denom_on_stride: Optional[str] = None,
261
+ ) -> Union[ChainConfig, StrideChainConfig, HostChainConfig]:
262
+ """
263
+ Fetch info about a host zone by name, id, ticker, denom, or token hash.
264
+
265
+ Raises
266
+ KeyError if a valid chain doesn't exist
267
+ """
268
+ if sum(map(bool, [name, id, ticker, denom, denom_on_stride])) != 1:
269
+ raise KeyError("Exactly one of name, id, ticker, denom, or token hash must be specified.")
270
+
271
+ if (
272
+ name == self.stride.name
273
+ or id == self.stride.id
274
+ or ticker == self.stride.ticker
275
+ or denom == self.stride.denom
276
+ ):
277
+ return self.stride
278
+ elif name:
279
+ return self._name_to_zone[name]
280
+ elif id:
281
+ return self._id_to_zone[id]
282
+ elif ticker:
283
+ return self._ticker_to_zone[ticker]
284
+ elif denom:
285
+ return self._denom_to_zone[denom]
286
+ return self._denom_on_stride_to_zone[denom_on_stride]
287
+
288
+
289
+ def _load_raw_config() -> dict[str, Any]:
290
+ """
291
+ Loads the raw config yaml into a dictionary
292
+ If STRIDEUTILS_CONFIG_PATH is set, it uses that for the config path, otherwise, it
293
+ looks for a config/config.yaml
294
+ """
295
+ strideutils_config_path = os.environ.get('STRIDEUTILS_CONFIG_PATH', 'config/config.yaml')
296
+ if not os.path.exists(strideutils_config_path):
297
+ raise ValueError(
298
+ 'Shell var STRIDEUTILS_CONFIG_PATH is missing or the file does not exist in the default location.'
299
+ 'The default file and location is config/config.yaml in the current working directory'
300
+ )
301
+
302
+ with open(strideutils_config_path, 'r') as config_file:
303
+ raw_config = yaml.load(config_file, Loader)
304
+
305
+ return raw_config
306
+
307
+
308
+ def _load_raw_secrets() -> dict[str, Optional[str]]:
309
+ """
310
+ Loads the secret variables into a dictionary
311
+ If this is running locally in dev mode, secrets are grabbed from the path defined by STRIDEUTILS_SECRETS_PATH
312
+ Otherwise, their loaded as environment variables
313
+ """
314
+ if ENV == "PROD":
315
+ return {secret.name: os.environ.get(secret.name) for secret in fields(Config) if os.environ.get(secret.name)}
316
+
317
+ if not (strideutils_secrets_path := os.environ.get('STRIDEUTILS_SECRETS_PATH')):
318
+ raise ValueError(MISSING_CONFIG_MSG.format(x='STRIDEUTILS_SECRETS_PATH'))
319
+
320
+ with open(strideutils_secrets_path, 'r') as f:
321
+ raw_secrets = yaml.load(f, Loader)
322
+ raw_secrets.update({'ENV': ENV})
323
+
324
+ return raw_secrets
325
+
326
+
327
+ def _parse_raw_configs(raw_config: dict[str, Any], raw_secrets: dict[str, Optional[str]]) -> Config:
328
+ """
329
+ Builds the relevant config data classes from the raw jsons
330
+ """
331
+ host_chain_fields = [field.name for field in fields(HostChainsConfig)]
332
+ app_chain_fields = [field.name for field in fields(AppChainsConfig)]
333
+
334
+ host_chain_config_dict = {
335
+ name: HostChainConfig(**info) for name, info in raw_config["host_zones"].items() if name in host_chain_fields
336
+ }
337
+ app_chain_config_dict = {
338
+ name: ChainConfig(**info) for name, info in raw_config["app_chains"].items() if name in app_chain_fields
339
+ }
340
+
341
+ stride_chain_config = StrideChainConfig(**raw_config["stride"])
342
+ host_zone_config = HostChainsConfig(**host_chain_config_dict)
343
+ app_chain_config = AppChainsConfig(**app_chain_config_dict)
344
+
345
+ chain_configs = {
346
+ 'stride': stride_chain_config,
347
+ 'host_zones': host_zone_config,
348
+ 'app_chains': app_chain_config,
349
+ }
350
+
351
+ # Load non-nested configs, then overwrite with the chain configs
352
+ config_dict = raw_secrets
353
+ config_dict.update(raw_config)
354
+ config_dict.update(chain_configs)
355
+ return Config(**config_dict) # type: ignore
356
+
357
+
358
+ def _init_config() -> Config:
359
+ """
360
+ Initializes the main config files
361
+ """
362
+ raw_config = _load_raw_config()
363
+ raw_secrets = _load_raw_secrets()
364
+ return _parse_raw_configs(raw_config, raw_secrets)
365
+
366
+
367
+ config: Config = _init_config()
@@ -40,6 +40,9 @@ RATE_LIMIT_QUERY = "Stride-Labs/ibc-rate-limiting/ratelimit/ratelimits"
40
40
  STAKETIA = "staketia"
41
41
  STAKEDYM = "stakedym"
42
42
 
43
+ CELESTIA_CHAIN_ID = "celestia"
44
+ DYMENSION_CHAIN_ID = "dymension_1100-1"
45
+
43
46
  MULTISIG_HOST_ZONE_QUERY = "Stride-Labs/stride/{module}/host_zone"
44
47
  MULTISIG_DELEGATION_RECORDS_QUERY = "Stride-Labs/stride/{module}/delegation_records"
45
48
  MULTISIG_UNBONDING_RECORDS_QUERY = "Stride-Labs/stride/{module}/unbonding_records"
@@ -138,9 +141,11 @@ def query_list_with_pagination(
138
141
  return data
139
142
 
140
143
 
141
- def get_all_host_zones() -> List[Dict]:
144
+ def get_all_host_zones(include_multisig_hosts: bool = True) -> List[Dict]:
142
145
  """
143
146
  Queries all host zone structs, returning a list of each host zone json
147
+ By default, this queries both stakeibc and staketia/stakedym; however,
148
+ staketia/stakedym can be disabled by passing include_multisig_hosts = False
144
149
 
145
150
  Returns:
146
151
  A list of host zone structs
@@ -184,11 +189,19 @@ def get_all_host_zones() -> List[Dict]:
184
189
  ...more host zones]
185
190
 
186
191
  """
187
- endpoint = f"{config.stride.api_endpoint}/{HOST_ZONE_QUERY}"
188
- response = request(endpoint)
189
- host_zones = response["host_zone"]
192
+ stakeibc_response = request(f"{config.stride.api_endpoint}/{HOST_ZONE_QUERY}")
193
+ stakeibc_host_zones = stakeibc_response["host_zone"]
194
+
195
+ if not include_multisig_hosts:
196
+ return stakeibc_host_zones
197
+
198
+ staketia_host_zone = get_host_zone(CELESTIA_CHAIN_ID, cast_types=False)
199
+ stakedym_host_zone = get_host_zone(DYMENSION_CHAIN_ID, cast_types=False)
200
+
201
+ stakedym_response = request(f"{config.stride.api_endpoint}/{MULTISIG_HOST_ZONE_QUERY.format(module=STAKEDYM)}")
202
+ stakedym_host_zone = stakedym_response["host_zone"]
190
203
 
191
- return host_zones
204
+ return stakeibc_host_zones + [staketia_host_zone, stakedym_host_zone]
192
205
 
193
206
 
194
207
  def _cast_host_zone_fields(host_zone: Dict[str, Any]) -> Dict[str, Any]:
@@ -213,17 +226,17 @@ def _cast_host_zone_fields(host_zone: Dict[str, Any]) -> Dict[str, Any]:
213
226
  return host_zone
214
227
 
215
228
 
216
- def get_host_zone_json(zone_id: str, cast_types: bool = True, **kwargs) -> Dict[str, Any]:
229
+ def get_host_zone(chain_id: str, cast_types: bool = True, **kwargs) -> Dict[str, Any]:
217
230
  """
218
231
  Queries a stakeibc host zone, returning a dict with the host zone structure and fields casted
219
232
  """
220
- if zone_id == "celestia":
221
- return get_celestia_host_zone(cast_types=cast_types)
222
- if zone_id == "dymension_1100-1":
223
- return get_dymension_host_zone(cast_types=cast_types)
233
+ query = f"Stride-Labs/stride/stakeibc/host_zone/{chain_id}"
234
+ if chain_id in [CELESTIA_CHAIN_ID, DYMENSION_CHAIN_ID]:
235
+ query = MULTISIG_HOST_ZONE_QUERY.format(module=STAKETIA)
236
+ if chain_id == "dymension_1100-1":
237
+ query = MULTISIG_HOST_ZONE_QUERY.format(module=STAKEDYM)
224
238
 
225
- endpoint = f"{config.stride.api_endpoint}/Stride-Labs/stride/stakeibc/host_zone/{zone_id}"
226
- host_zone = request(endpoint, **kwargs)["host_zone"]
239
+ host_zone = request(f"{config.stride.api_endpoint}/{query}", **kwargs)["host_zone"]
227
240
 
228
241
  if cast_types:
229
242
  host_zone = _cast_host_zone_fields(host_zone)
@@ -231,77 +244,6 @@ def get_host_zone_json(zone_id: str, cast_types: bool = True, **kwargs) -> Dict[
231
244
  return host_zone
232
245
 
233
246
 
234
- # TODO: Consolidate celestia and dymension queries (API breaking)
235
- def get_celestia_host_zone(cast_types: bool = True) -> Dict[str, Any]:
236
- """
237
- Queries the celestia host zone from staketia
238
-
239
- Example response:
240
- {
241
- 'chain_id': 'celestia',
242
- 'native_token_denom': 'utia',
243
- 'native_token_ibc_denom': 'ibc/BF3B4F53F3694B66E13C23107C84B6485BD2B96296BB7EC680EA77BBA75B4801',
244
- 'transfer_channel_id': 'channel-162',
245
- 'delegation_address': 'celestia1d6ntc7s8gs86tpdyn422vsqc6uaz9cejnxz5p5',
246
- 'reward_address': 'celestia15up3hegy8zuqhy0p9m8luh0c984ptu2g5p4xpf',
247
- 'deposit_address': 'stride1d6ntc7s8gs86tpdyn422vsqc6uaz9cejp8nc04',
248
- 'redemption_address': 'stride15up3hegy8zuqhy0p9m8luh0c984ptu2gxqy20g',
249
- 'claim_address': 'stride13nw9fm4ua8pwzmsx9kdrhefl4puz0tp7ge3gxd',
250
- 'operator_address_on_stride': 'stride1ghhu67ttgmxrsyxljfl2tysyayswklvxs7pepw',
251
- 'safe_address_on_stride': 'stride18p7xg4hj2u3zpk0v9gq68pjyuuua5wa387sjjc',
252
- 'last_redemption_rate': '1.000000000000000000',
253
- 'redemption_rate': '1.000000000000000000',
254
- 'min_redemption_rate': '0.950000000000000000',
255
- 'max_redemption_rate': '1.100000000000000000',
256
- 'min_inner_redemption_rate': '0.950000000000000000',
257
- 'max_inner_redemption_rate': '1.100000000000000000',
258
- 'delegated_balance': '300000',
259
- 'unbonding_period_seconds': '1814400',
260
- 'halted': False
261
- }
262
- """
263
- url = f"{config.stride.api_endpoint}/{MULTISIG_HOST_ZONE_QUERY.format(module=STAKETIA)}"
264
- host_zone = request(url)["host_zone"]
265
- if cast_types:
266
- _cast_host_zone_fields(host_zone)
267
- return host_zone
268
-
269
-
270
- def get_dymension_host_zone(cast_types: bool = True) -> Dict[str, Any]:
271
- """
272
- Queries the dymension host zone from stakedym
273
-
274
- Example response:
275
- {
276
- 'chain_id': 'dymension',
277
- 'native_token_denom': 'adym',
278
- 'native_token_ibc_denom': 'ibc/E1C22332C083574F3418481359733BA8887D171E76C80AD9237422AEABB66018',
279
- 'transfer_channel_id': 'channel-197',
280
- 'delegation_address': 'dym1gl9j2hyyukqvlmzzcxl99mqfgu4y4frgzlv3zz',
281
- 'reward_address': 'dym1ww3m6h5e3dk2musft9w8f2zu4rkuxgh6zwu0d0',
282
- 'deposit_address': 'stride1e7j8d6sdq272fqe2jfxjpgcagn04j75w9695fj',
283
- 'redemption_address': 'stride1jpsnc0ynufa2aheflj6mxzzzsu7nlwqk7ff69n',
284
- 'claim_address': 'stride1q8juddwptg5yxyghh3n243pp4w8ctpvpmf6ras',
285
- 'operator_address_on_stride': 'stride1ghhu67ttgmxrsyxljfl2tysyayswklvxs7pepw',
286
- 'safe_address_on_stride': 'stride1sj8gyqeqecqhqu7em67hn2tjzhpkdf8wz5plh7',
287
- 'last_redemption_rate': '1.000000000000000000',
288
- 'redemption_rate': '1.000000000000000000',
289
- 'min_redemption_rate': '0.950000000000000000',
290
- 'max_redemption_rate': '1.100000000000000000',
291
- 'min_inner_redemption_rate': '0.950000000000000000',
292
- 'max_inner_redemption_rate': '1.100000000000000000',
293
- 'delegated_balance': '300000',
294
- 'unbonding_period_seconds': '1814400',
295
- 'halted': False
296
- }
297
- """
298
- url = f"{config.stride.api_endpoint}/{MULTISIG_HOST_ZONE_QUERY.format(module=STAKEDYM)}"
299
- host_zone = request(url)["host_zone"]
300
- if cast_types:
301
- _cast_host_zone_fields(host_zone)
302
- return host_zone
303
-
304
-
305
247
  def get_multisig_delegation_records(module: str, include_archived: bool = False) -> List[Dict]:
306
248
  """
307
249
  Queries the delegation records from staketia or stakedym
@@ -419,20 +361,18 @@ def get_redemption_rate(token: str) -> float:
419
361
  """
420
362
  Queries the redemption rate for a particular token
421
363
  """
422
- js = get_host_zone_json(config.get_chain(ticker=token).id)
423
- return float(js["redemption_rate"])
364
+ host_zone = get_host_zone(config.get_chain(ticker=token).id)
365
+ return float(host_zone["redemption_rate"])
424
366
 
425
367
 
426
368
  def get_tvl_in_utokens(host_zone_id: str) -> float:
427
369
  """
428
370
  Queries the stride TVL for a given zone (denominated in the native token)
429
371
  """
430
- zone_info = config.get_chain(id=host_zone_id)
431
- js = get_host_zone_json(host_zone_id)
432
- delegation_key = "total_delegations"
433
- if delegation_key not in js:
434
- delegation_key = "delegated_balance"
435
- return int(js[delegation_key]) / pow(10, zone_info.denom_decimals)
372
+ host_zone_config = config.get_chain(id=host_zone_id)
373
+ host_zone = get_host_zone(host_zone_id)
374
+ delegation_key = "total_delegations" if host_zone_config.ica_enabled else "delegated_balance"
375
+ return int(host_zone[delegation_key]) / pow(10, host_zone_config.denom_decimals)
436
376
 
437
377
 
438
378
  def get_latest_block(rpc_endpoint: str = config.stride.rpc_endpoint) -> int:
@@ -460,7 +400,7 @@ def convert_cosmos_address(address: str, new_prefix: str) -> str:
460
400
  def get_balances(
461
401
  address: str,
462
402
  block_height: int = 0,
463
- api_endpoint: str = config.stride.library_api,
403
+ api_endpoint: str = config.stride.api_endpoint,
464
404
  cache_response: bool = False,
465
405
  ) -> Dict[str, int]:
466
406
  """
@@ -476,7 +416,7 @@ def get_balance(
476
416
  address: str,
477
417
  denom: str,
478
418
  block_height: int = 0,
479
- api_endpoint: str = config.stride.library_api,
419
+ api_endpoint: str = config.stride.api_endpoint,
480
420
  cache_response: bool = False,
481
421
  ) -> int:
482
422
  """
@@ -504,7 +444,7 @@ def get_supply(
504
444
 
505
445
 
506
446
  @lru_cache
507
- def get_block_time(height: int, rpc_endpoint: str = config.stride.library_rpc) -> datetime.datetime:
447
+ def get_block_time(height: int, rpc_endpoint: str = config.stride.library_rpc_endpoint) -> datetime.datetime:
508
448
  """
509
449
  Returns the timestamp of the given block height on a chain
510
450
  """
@@ -615,7 +555,7 @@ def generate_vesting_account(
615
555
 
616
556
  The output is a JSON object that can be used to create a vesting account on the Stride chain.
617
557
  """
618
- timezone = pytz.timezone(config.TIMEZONE)
558
+ timezone = pytz.timezone(config.timezone)
619
559
  start_time = timezone.localize(datetime.datetime.strptime(start_time_in_local_tz, "%Y-%m-%d %H:%M"))
620
560
  start_time = start_time.astimezone(pytz.utc)
621
561
  start_time = int(start_time.timestamp())
@@ -643,7 +583,7 @@ def get_block_height_in_past(
643
583
  seconds_per_block: int = 6,
644
584
  prev_time: Optional[str] = None,
645
585
  verbose: bool = False,
646
- rpc_endpoint: str = config.stride.library_rpc,
586
+ rpc_endpoint: str = config.stride.library_rpc_endpoint,
647
587
  ) -> int:
648
588
  """
649
589
  Gets the block height at a specified time by binary searching at different heights
@@ -786,7 +726,7 @@ def get_host_zone_delegations(host_zone_id: str, ground_truth_included: bool = T
786
726
  If `ground_truth_included=True`, then will also return the "true" delegations
787
727
  on the host zone delegaiton account.
788
728
  """
789
- host_zone = get_host_zone_json(host_zone_id)
729
+ host_zone = get_host_zone(host_zone_id)
790
730
  vdf = pd.DataFrame(host_zone["validators"])
791
731
  int_cols = [
792
732
  "weight",
@@ -1,35 +0,0 @@
1
- """
2
- Exposes an easy API to get prices from coingecko
3
- """
4
-
5
- from strideutils import stride_requests
6
- from strideutils.stride_config import config
7
-
8
- COINGECKO_ENDPOINT = "https://pro-api.coingecko.com/"
9
- COINGECKO_PRICE_QUERY = "api/v3/simple/price?ids={ticker_id}&vs_currencies=usd"
10
-
11
-
12
- def get_token_price(
13
- ticker: str,
14
- api_token: str = config.COINGECKO_API_TOKEN,
15
- cache_response: bool = False,
16
- ):
17
- """
18
- Reads token price from coingecko
19
- If a redis_client is provided, then store the value.
20
- """
21
- # TODO: Consider using the coingecko ID for stTokens instead of the redemption rate
22
- # Get redemption rate for calculating st token prices
23
- redemption_rate = float(1)
24
- if ticker.startswith('st') and ticker[3].isupper():
25
- redemption_rate = stride_requests.get_redemption_rate(ticker[2:])
26
- ticker = ticker[2:]
27
-
28
- coingecko_name = config.get_chain(ticker=ticker).coingecko_name
29
- endpoint = COINGECKO_ENDPOINT + COINGECKO_PRICE_QUERY.format(ticker_id=coingecko_name)
30
- headers = {'x-cg-pro-api-key': api_token}
31
-
32
- response = stride_requests.request(endpoint, headers=headers, cache_response=cache_response)
33
- price = response[coingecko_name]["usd"] * redemption_rate
34
-
35
- return price
@@ -1,301 +0,0 @@
1
- """
2
- This file loads and provide an object `config` to access all secrets, configs, and chain info
3
- """
4
-
5
- import itertools
6
- import os
7
- from dataclasses import dataclass, field, fields
8
- from typing import List, Optional, Union
9
-
10
- import yaml
11
-
12
- MISSING_CONFIG_MSG = "Configuration or environment variable {x} is not set or unknown"
13
-
14
- ENV = None
15
- try:
16
- ENV = os.environ['ENV']
17
- except KeyError:
18
- raise ValueError(MISSING_CONFIG_MSG.format(x='ENV'))
19
-
20
-
21
- class ConfigObj:
22
- """Raise an error if a config is not set."""
23
-
24
- def __getattr__(self, name):
25
- raise AttributeError(MISSING_CONFIG_MSG.format(x=name))
26
-
27
- def __getattribute__(self, name):
28
- value = super().__getattribute__(name)
29
- if value == "" or value is None:
30
- raise AttributeError # induces a call to __getattr__
31
- return value
32
-
33
- def __iter__(self):
34
- """Allow iterating over set values"""
35
- for subfield in fields(self):
36
- try:
37
- value = getattr(self, subfield.name)
38
- except AttributeError:
39
- continue
40
- yield value
41
-
42
-
43
- class ConfigDict(dict):
44
- def __getitem__(self, key):
45
- """Raise an error if an unset key is indexed."""
46
- try:
47
- value = super().__getitem__(key)
48
- if value == "" or value is None:
49
- raise KeyError
50
- return value
51
- except KeyError:
52
- raise KeyError(MISSING_CONFIG_MSG.format(x=key))
53
-
54
-
55
- # Use ConfigDict in the yaml parser
56
- class Loader(yaml.FullLoader):
57
- def construct_yaml_map(self, node):
58
- data = ConfigDict()
59
- yield data
60
- value = self.construct_mapping(node)
61
- data.update(value)
62
-
63
-
64
- Loader.add_constructor('tag:yaml.org,2002:map', Loader.construct_yaml_map)
65
-
66
-
67
- @dataclass(repr=False)
68
- class ChainConfig(ConfigObj):
69
- name: str = ""
70
- id: str = ""
71
- # Optional. For looking up prices on coingecko if it differs from name
72
- coingecko_name: str = ""
73
- denom: str = ""
74
- denom_decimals: int = 6
75
- inflation_frequency_hours: int = -1
76
- ticker: str = ""
77
- stride_token_hash: str = ""
78
- api_endpoint: str = ""
79
- rpc_endpoint: str = ""
80
- library_api: str = ""
81
- library_rpc: str = ""
82
-
83
-
84
- @dataclass(repr=False)
85
- class HostZoneChainConfig(ConfigObj):
86
- cosmoshub: ChainConfig = field(default_factory=ChainConfig)
87
- osmosis: ChainConfig = field(default_factory=ChainConfig)
88
- evmos: ChainConfig = field(default_factory=ChainConfig)
89
- injective: ChainConfig = field(default_factory=ChainConfig)
90
- juno: ChainConfig = field(default_factory=ChainConfig)
91
- stargaze: ChainConfig = field(default_factory=ChainConfig)
92
- terra: ChainConfig = field(default_factory=ChainConfig)
93
- umee: ChainConfig = field(default_factory=ChainConfig)
94
- comdex: ChainConfig = field(default_factory=ChainConfig)
95
- sommelier: ChainConfig = field(default_factory=ChainConfig)
96
- dydx: ChainConfig = field(default_factory=ChainConfig)
97
- saga: ChainConfig = field(default_factory=ChainConfig)
98
-
99
-
100
- @dataclass(repr=False)
101
- class AppConfig(ConfigObj):
102
- neutron: ChainConfig = field(default_factory=ChainConfig)
103
- kava: ChainConfig = field(default_factory=ChainConfig)
104
- iris: ChainConfig = field(default_factory=ChainConfig)
105
- akash: ChainConfig = field(default_factory=ChainConfig)
106
- sentinel: ChainConfig = field(default_factory=ChainConfig)
107
- persistence: ChainConfig = field(default_factory=ChainConfig)
108
- cryptoorg: ChainConfig = field(default_factory=ChainConfig)
109
- kichain: ChainConfig = field(default_factory=ChainConfig)
110
- bitcanna: ChainConfig = field(default_factory=ChainConfig)
111
- regen: ChainConfig = field(default_factory=ChainConfig)
112
- gravity: ChainConfig = field(default_factory=ChainConfig)
113
- desmos: ChainConfig = field(default_factory=ChainConfig)
114
- chihuahua: ChainConfig = field(default_factory=ChainConfig)
115
- secret: ChainConfig = field(default_factory=ChainConfig)
116
- axelar: ChainConfig = field(default_factory=ChainConfig)
117
- crescent: ChainConfig = field(default_factory=ChainConfig)
118
- mantle: ChainConfig = field(default_factory=ChainConfig)
119
- mars: ChainConfig = field(default_factory=ChainConfig)
120
- canto: ChainConfig = field(default_factory=ChainConfig)
121
- agoric: ChainConfig = field(default_factory=ChainConfig)
122
- celestia: ChainConfig = field(default_factory=ChainConfig)
123
- dymension: ChainConfig = field(default_factory=ChainConfig)
124
-
125
-
126
- @dataclass(repr=False)
127
- class Config(ConfigObj):
128
- ENV: str
129
- timezone: str = "US/Eastern"
130
-
131
- # Twilio
132
- TWILIO_ALERTS_NUMBER: str = ""
133
- TWILIO_ACCOUNT_ID: str = ""
134
- TWILIO_API_TOKEN: str = ""
135
- PHONE_NUMBERS: ConfigDict = field(default_factory=ConfigDict)
136
-
137
- # Slack
138
- STRIDEBOT_API_TOKEN: str = ""
139
-
140
- # Gsheets - loads from str env var in PROD
141
- PUBLICSHEETS_AUTH: Union[str, ConfigDict] = field(default_factory=ConfigDict)
142
-
143
- # Coingecko
144
- COINGECKO_API_TOKEN: str = ""
145
-
146
- # Protocol Staking (dydx yield)
147
- PROTOCOL_STAKING_API_TOKEN: str = ""
148
-
149
- # Stride alerts
150
- alerts_playbook: str = ""
151
- slack_channels: ConfigDict = field(default_factory=ConfigDict)
152
-
153
- # Upstash
154
- UPSTASH_PUBLIC_PASSWORD: str = ""
155
- UPSTASH_STRIDE_FRONTEND_PASSWORD: str = ""
156
- UPSTASH_STRIDE_BACKEND_PASSWORD: str = ""
157
- UPSTASH_STRIDE_DYDX_PUBLIC_PASSWORD: str = ""
158
-
159
- # Stride internal secrets
160
- SLACK_BEARER_TOKEN: str = ""
161
- SLACK_SUCCESS_CHANNEL_ID: str = ""
162
- SLACK_INVARIANT_FAILURE_CHANNEL_ID: str = ""
163
- SLACK_PACKET_FAILURE_CHANNEL_ID: str = ""
164
- NUMIA_API_TOKEN: str = ""
165
- founders: List[str] = field(default_factory=lambda: ['riley', 'aidan', 'vishal'])
166
-
167
- # Chain configs
168
- stride: ChainConfig = field(default_factory=ChainConfig)
169
- host_zones: HostZoneChainConfig = field(default_factory=HostZoneChainConfig)
170
- app_chains: AppConfig = field(default_factory=AppConfig)
171
-
172
- def _create_mappings(self) -> None:
173
- """
174
- Create a reverse map from attributes to ChainConfig if id, ticker, and denom are set.
175
- """
176
- name_to_zone = ConfigDict()
177
- id_to_zone = ConfigDict()
178
- ticker_to_zone = ConfigDict()
179
- denom_to_zone = ConfigDict()
180
- stride_token_hash_to_zone = ConfigDict()
181
-
182
- # Map host zones
183
- for chain_info in itertools.chain(self.host_zones, self.app_chains):
184
- name_to_zone[chain_info.name] = chain_info
185
-
186
- try:
187
- id_to_zone[chain_info.id] = chain_info
188
- except AttributeError:
189
- pass
190
-
191
- try:
192
- ticker_to_zone[chain_info.ticker] = chain_info
193
- ticker_to_zone[chain_info.ticker.upper()] = chain_info
194
- ticker_to_zone[chain_info.ticker.lower()] = chain_info
195
- ticker_to_zone[chain_info.ticker.denom[1].upper()] = chain_info
196
- except AttributeError:
197
- pass
198
-
199
- try:
200
- denom_to_zone[chain_info.denom] = chain_info
201
- except AttributeError:
202
- pass
203
-
204
- try:
205
- stride_token_hash_to_zone[chain_info.stride_token_hash] = chain_info
206
- except AttributeError:
207
- pass
208
-
209
- self._name_to_zone = name_to_zone
210
- self._id_to_zone = id_to_zone
211
- self._ticker_to_zone = ticker_to_zone
212
- self._denom_to_zone = denom_to_zone
213
- self._stride_token_hash = stride_token_hash_to_zone
214
-
215
- def __post_init__(self):
216
- self._create_mappings()
217
-
218
- def get_chain( # type: ignore[return]
219
- self,
220
- name: Optional[str] = None,
221
- id: Optional[str] = None,
222
- ticker: Optional[str] = None,
223
- denom: Optional[str] = None,
224
- stride_token_hash: Optional[str] = None,
225
- ) -> ChainConfig:
226
- """
227
- Fetch info about a host zone by name, id, ticker, denom, or token hash.
228
-
229
- Raises
230
- KeyError if a valid chain doesn't exist
231
- """
232
- if sum(map(bool, [name, id, ticker, denom, stride_token_hash])) != 1:
233
- raise ValueError("Exactly one of name, id, ticker, denom, or token hash must be specified.")
234
-
235
- if (
236
- name == self.stride.name
237
- or id == self.stride.id
238
- or ticker == self.stride.ticker
239
- or denom == self.stride.denom
240
- ):
241
- return self.stride
242
- elif name:
243
- return self._name_to_zone[name]
244
- elif id:
245
- return self._id_to_zone[id]
246
- elif ticker:
247
- return self._ticker_to_zone[ticker]
248
- elif denom:
249
- return self._denom_to_zone[denom]
250
- elif stride_token_hash:
251
- return self._stride_token_hash[stride_token_hash]
252
-
253
-
254
- # Load chain config
255
- def _load() -> Config:
256
- """
257
- Load config dataclasses based on environment
258
- """
259
- STRIDEUTILS_CONFIG_PATH = os.environ.get('STRIDEUTILS_CONFIG_PATH', 'config/config.yaml')
260
- if not os.path.exists(STRIDEUTILS_CONFIG_PATH):
261
- raise ValueError(
262
- 'Shell var STRIDEUTILS_CONFIG_PATH is missing or the file does not exist.'
263
- 'The default file and location is config/config.yaml in the current working directory'
264
- )
265
-
266
- with open(STRIDEUTILS_CONFIG_PATH, 'r') as f:
267
- raw_config = yaml.load(f, Loader)
268
- stride_chain_config = ChainConfig(**raw_config.get('stride'))
269
- host_zone_config = HostZoneChainConfig(
270
- **{name: ChainConfig(**info) for name, info in raw_config.get('host_zones').items()}
271
- )
272
- app_chain_config = AppConfig(
273
- **{name: ChainConfig(**info) for name, info in raw_config.get('app_chains').items()}
274
- )
275
- chain_configs = {
276
- 'stride': stride_chain_config,
277
- 'host_zones': host_zone_config,
278
- 'app_chains': app_chain_config,
279
- }
280
-
281
- # Load secrets
282
- if ENV == 'DEV':
283
- if not (STRIDEUTILS_SECRETS_PATH := os.environ.get('STRIDEUTILS_SECRETS_PATH')):
284
- raise ValueError(MISSING_CONFIG_MSG.format(x='STRIDEUTILS_SECRETS_PATH'))
285
- with open(STRIDEUTILS_SECRETS_PATH, 'r') as f:
286
- raw_secrets = yaml.load(f, Loader)
287
- raw_secrets.update({'ENV': ENV})
288
- elif ENV == 'PROD':
289
- raw_secrets = {
290
- secret.name: os.environ.get(secret.name) for secret in fields(Config) if os.environ.get(secret.name)
291
- }
292
-
293
- # Load vanilla configs
294
- raw_secrets.update(raw_config)
295
- # Overwrite prepped ChainConfigs
296
- raw_secrets.update(chain_configs)
297
- config = Config(**raw_secrets)
298
- return config
299
-
300
-
301
- config: Config = _load()
File without changes