strideutils 0.3.6__tar.gz → 0.3.15__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 (33) hide show
  1. {strideutils-0.3.6 → strideutils-0.3.15}/PKG-INFO +2 -1
  2. {strideutils-0.3.6 → strideutils-0.3.15}/pyproject.toml +2 -1
  3. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils/coingecko.py +30 -11
  4. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils/config_examples/strideutils_secrets.yaml.example +5 -1
  5. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils/exec_tx.py +2 -2
  6. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils/run_safely.py +5 -3
  7. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils/stride_config.py +9 -1
  8. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils/stride_requests.py +101 -41
  9. {strideutils-0.3.6 → strideutils-0.3.15}/README.md +0 -0
  10. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils/Makefile +0 -0
  11. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils/__init__.py +0 -0
  12. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils/config_examples/strideutils_config.yaml.example +0 -0
  13. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils/cryptography.py +0 -0
  14. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils/invariant_helpers.py +0 -0
  15. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils/sheets_connector.py +0 -0
  16. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils/slack_connector.py +0 -0
  17. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils/stride_alerts.py +0 -0
  18. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils/stride_upstash.py +0 -0
  19. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils/twilio_connector.py +0 -0
  20. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils/types.py +0 -0
  21. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils-stubs/__init__.pyi +0 -0
  22. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils-stubs/coingecko.pyi +0 -0
  23. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils-stubs/cryptography.pyi +0 -0
  24. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils-stubs/exec_tx.pyi +0 -0
  25. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils-stubs/invariant_helpers.pyi +0 -0
  26. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils-stubs/run_safely.pyi +0 -0
  27. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils-stubs/sheets_connector.pyi +0 -0
  28. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils-stubs/slack_connector.pyi +0 -0
  29. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils-stubs/stride_alerts.pyi +0 -0
  30. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils-stubs/stride_config.pyi +0 -0
  31. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils-stubs/stride_requests.pyi +0 -0
  32. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils-stubs/stride_upstash.pyi +0 -0
  33. {strideutils-0.3.6 → strideutils-0.3.15}/strideutils-stubs/twilio_connector.pyi +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: strideutils
3
- Version: 0.3.6
3
+ Version: 0.3.15
4
4
  Summary:
5
5
  Author: Your Name
6
6
  Author-email: you@example.com
@@ -9,6 +9,7 @@ Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3.11
10
10
  Classifier: Programming Language :: Python :: 3.12
11
11
  Requires-Dist: bech32 (>=1.2.0)
12
+ Requires-Dist: boto3 (>=1.34.115,<2.0.0)
12
13
  Requires-Dist: google-api-python-client (>=2.100.0)
13
14
  Requires-Dist: google-auth-httplib2 (>=0.1.1)
14
15
  Requires-Dist: google-auth-oauthlib (>=1.1.0)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "strideutils"
3
- version = "0.3.6"
3
+ version = "0.3.15"
4
4
  description = ""
5
5
  authors = ["Your Name <you@example.com>"]
6
6
  readme = "README.md"
@@ -24,6 +24,7 @@ tabulate = ">=0.9.0"
24
24
  pyyaml = ">=6.0.1"
25
25
  python-dateutil = ">=2.8.2"
26
26
  pycryptodome = ">=3.20.0"
27
+ boto3 = "^1.34.115"
27
28
 
28
29
  [tool.poetry.group.dev.dependencies]
29
30
  pytest = "^8.1.1"
@@ -12,6 +12,7 @@ from strideutils.stride_config import config
12
12
 
13
13
  COINGECKO_ENDPOINT = "https://pro-api.coingecko.com/"
14
14
  COINGECKO_PRICE_QUERY = "api/v3/simple/price?ids={ticker_id}&vs_currencies=usd"
15
+ COINGECKO_TVL_QUERY = "api/v3/coins/{ticker_id}"
15
16
 
16
17
 
17
18
  def get_coingecko_name(chain_config: stride_config.ChainConfig) -> str:
@@ -31,7 +32,6 @@ def get_token_price(
31
32
  ):
32
33
  """
33
34
  Reads token price from coingecko
34
- If a redis_client is provided, then store the value.
35
35
  """
36
36
  # TODO: Consider using the coingecko ID for stTokens instead of the redemption rate
37
37
  # Get redemption rate for calculating st token prices
@@ -53,6 +53,23 @@ def get_token_price(
53
53
  return price
54
54
 
55
55
 
56
+ def get_tvl(
57
+ chain_config: stride_config.ChainConfig,
58
+ api_token: str = config.COINGECKO_API_TOKEN,
59
+ cache_response: bool = False,
60
+ ) -> float:
61
+ """
62
+ Fetch TVL from coingecko in USD
63
+ """
64
+ coingecko_name = get_coingecko_name(chain_config)
65
+ endpoint = COINGECKO_ENDPOINT + COINGECKO_TVL_QUERY.format(ticker_id=coingecko_name)
66
+ headers = {'x-cg-pro-api-key': api_token}
67
+
68
+ # The data structure is huge https://docs.coingecko.com/reference/coins-id
69
+ response = stride_requests.request(endpoint, headers=headers, cache_response=cache_response)
70
+ return float(response['market_data']['market_cap']['usd'])
71
+
72
+
56
73
  def get_token_price_history(token: str, num_days=90, use_key=False, memo={}) -> pd.Series:
57
74
  """
58
75
  returns a Pandas Series with token price data going back num_days
@@ -62,17 +79,18 @@ def get_token_price_history(token: str, num_days=90, use_key=False, memo={}) ->
62
79
  price_key = (token, num_days)
63
80
  if price_key not in memo:
64
81
  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
82
+ rurl = (
83
+ f"https://api.coingecko.com/api/v3/coins/{tstr}/market_chart?vs_currency=usd&days={num_days}" # noqa:E231
84
+ )
85
+ rel_headers = {"x-cg-pro-api-key": config.COINGECKO_API_TOKEN} if use_key else None
86
+
69
87
  r = requests.get(
70
88
  rurl,
71
89
  headers=rel_headers,
72
90
  )
73
91
  assert r.status_code == 200
74
92
  out_df = pd.DataFrame(r.json()["prices"])
75
- out_df.columns = ["time", "price"]
93
+ out_df.columns = pd.Index(["time", "price"])
76
94
  out_df["time"] = pd.to_datetime(out_df["time"], unit="ms")
77
95
  out_df["price"] = out_df["price"].astype(float)
78
96
  out_df.set_index("time", inplace=True)
@@ -89,17 +107,18 @@ def get_token_volume_history(token: str, num_days=90, use_key=False, memo={}) ->
89
107
  volume_key = (token, num_days)
90
108
  if volume_key not in memo:
91
109
  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
110
+ rurl = (
111
+ f"https://api.coingecko.com/api/v3/coins/{tstr}/market_chart?vs_currency=usd&days={num_days}" # noqa:E231
112
+ )
113
+ rel_headers = {"x-cg-pro-api-key": config.COINGECKO_API_TOKEN} if use_key else None
114
+
96
115
  r = requests.get(
97
116
  rurl,
98
117
  headers=rel_headers,
99
118
  )
100
119
  assert r.status_code == 200
101
120
  out_df = pd.DataFrame(r.json()["total_volumes"])
102
- out_df.columns = ["time", "volume"]
121
+ out_df.columns = pd.Index(["time", "volume"])
103
122
  out_df["time"] = pd.to_datetime(out_df["time"], unit="ms")
104
123
  out_df["volume"] = out_df["volume"].astype(float)
105
124
  out_df.set_index("time", inplace=True)
@@ -21,4 +21,8 @@ SLACK_PACKET_FAILURE_CHANNEL_ID:
21
21
  NUMIA_API_TOKEN:
22
22
  UPSTASH_PUBLIC_PASSWORD:
23
23
  UPSTASH_STRIDE_FRONTEND_PASSWORD:
24
- UPSTASH_STRIDE_BACKEND_PASSWORD:
24
+ UPSTASH_STRIDE_BACKEND_PASSWORD:
25
+
26
+ # AWS
27
+ AWS_ACCESS_KEY_ID:
28
+ AWS_SECRET_ACCESS_KEY:
@@ -33,10 +33,10 @@ def execute_tx(command: str, api_endpoint: str = config.stride.api_endpoint) ->
33
33
 
34
34
  tx_response = None
35
35
  response_success = False
36
- raw_log = ""
36
+ raw_log = "Transaction not found on chain after one minute" # default message
37
37
  if not error_out:
38
38
  event_filters = [(f"tx.hash='{tx_hash}'")]
39
- for _ in range(15):
39
+ for _ in range(30):
40
40
  time.sleep(2)
41
41
  tx_response = stride_requests.get_txs(event_filters, api_endpoint)
42
42
  if tx_response['total'] != '0':
@@ -29,13 +29,15 @@ def try_and_log_with_status(
29
29
  # (to cover cases where click was used)
30
30
  func_name = getattr(func, 'name', None) or func.__name__
31
31
  try:
32
+ print(f"Running {func_name}...", end="")
32
33
  func()
33
- msg = [f':white_check_mark: {func_name}']
34
+ print("✅")
35
+ msgs += [f':white_check_mark: {func_name}']
34
36
  except Exception:
37
+ print("❌")
35
38
  print(traceback.format_exc())
36
- msg = [f':x: {func_name} ']
39
+ msgs += [f':x: {func_name} ']
37
40
  tracebacks.append(f"{func_name} \n\n" + traceback.format_exc())
38
- msgs += msg
39
41
 
40
42
  count_succeeded_msg = f"{len(functions) - len(tracebacks)}/{len(functions)} {job_name} succeeded"
41
43
  if len(tracebacks) > 0:
@@ -78,6 +78,7 @@ class ChainConfig(ConfigObj):
78
78
  ticker: str = ""
79
79
  api_endpoint: str = ""
80
80
  rpc_endpoint: str = ""
81
+ evm_chain: bool = False
81
82
 
82
83
  def __repr__(self) -> str:
83
84
  return f"ChainConfig(name={self.name}, id={self.id})"
@@ -152,6 +153,9 @@ class AppChainsConfig(ConfigObj):
152
153
  mars: ChainConfig = field(default_factory=ChainConfig)
153
154
  canto: ChainConfig = field(default_factory=ChainConfig)
154
155
  agoric: ChainConfig = field(default_factory=ChainConfig)
156
+ sei: ChainConfig = field(default_factory=ChainConfig)
157
+ noble: ChainConfig = field(default_factory=ChainConfig)
158
+ forma: ChainConfig = field(default_factory=ChainConfig)
155
159
 
156
160
 
157
161
  @dataclass(repr=False)
@@ -195,6 +199,10 @@ class Config(ConfigObj):
195
199
  NUMIA_API_TOKEN: str = ""
196
200
  founders: List[str] = field(default_factory=lambda: ['riley', 'aidan', 'vishal'])
197
201
 
202
+ # AWS
203
+ AWS_ACCESS_KEY_ID: str = ""
204
+ AWS_SECRET_ACCESS_KEY: str = ""
205
+
198
206
  # Chain configs
199
207
  stride: StrideChainConfig = field(default_factory=StrideChainConfig)
200
208
  host_zones: HostChainsConfig = field(default_factory=HostChainsConfig)
@@ -361,7 +369,7 @@ def _parse_raw_configs(raw_config: dict[str, Any], raw_secrets: dict[str, Option
361
369
  # Load non-nested configs, then overwrite with the chain configs
362
370
  config_dict = raw_secrets
363
371
  config_dict.update(raw_config)
364
- config_dict.update(chain_configs)
372
+ config_dict.update(chain_configs) # type: ignore
365
373
  return Config(**config_dict) # type: ignore
366
374
 
367
375
 
@@ -37,6 +37,7 @@ PENDING_ICQ_QUERY = "Stride-Labs/stride/interchainquery/pending_queries"
37
37
  RATE_LIMIT_QUERY = "Stride-Labs/ibc-rate-limiting/ratelimit/ratelimits"
38
38
  DEPOSIT_RECORDS_QUERY = "Stride-Labs/stride/records/deposit_record_by_host_zone/{chain_id}"
39
39
  UNBONDING_RECORDS_QUERY = "Stride-Labs/stride/records/epoch_unbonding_record"
40
+ REDEMPTION_RECORDS_QUERY = "Stride-Labs/stride/records/user_redemption_record"
40
41
  LSM_RECORDS_QUERY = "Stride-Labs/stride/stakeibc/lsm_deposits"
41
42
  CALLBACK_DATA_QUERY = "Stride-Labs/stride/icacallbacks/callback_data"
42
43
 
@@ -67,6 +68,7 @@ def request(
67
68
  url: str,
68
69
  headers: dict = {},
69
70
  params: Union[dict, List[Tuple[str, str]]] = {},
71
+ data: Optional[str] = None,
70
72
  block_height: int = 0,
71
73
  cache_response: bool = False,
72
74
  _cache: Dict[str, Any] = {},
@@ -95,7 +97,7 @@ def request(
95
97
  if type(params) is dict:
96
98
  params = list(params.items())
97
99
 
98
- resp = requests_session.get(url, headers=headers, params=params)
100
+ resp = requests_session.get(url, headers=headers, params=params, data=data)
99
101
  if not resp.ok: # resp.ok is true if state_code is less than 400
100
102
  print(f"Error fetching {url}: {resp.status_code} {resp.json()}")
101
103
  raise Exception(f"Error fetching {url}: {resp.status_code} {resp.json()}")
@@ -381,6 +383,33 @@ def get_unbonded_records(
381
383
  return filtered_unbonding_records
382
384
 
383
385
 
386
+ def get_user_redemption_records(chain_id: Optional[str] = None) -> list[dict]:
387
+ """
388
+ Queries all user redemption records with optional chain_id filter
389
+
390
+ [
391
+ {
392
+ "id": "comdex-1.601.comdex1fq0klaec7s59drpl93w6yycgqqtd3d38d34zn0",
393
+ "receiver": "comdex1fq0klaec7s59drpl93w6yycgqqtd3d38d34zn0",
394
+ "native_token_amount": "63303481323",
395
+ "denom": "ucmdx",
396
+ "host_zone_id": "comdex-1",
397
+ "epoch_number": "601",
398
+ "claim_is_pending": false,
399
+ "st_token_amount": "51025461480"
400
+ },
401
+ ...
402
+ """
403
+ url = f"{config.stride.api_endpoint}/{REDEMPTION_RECORDS_QUERY}"
404
+ redemption_records = query_list_with_pagination(url, rel_key="user_redemption_record", max_pages=50)
405
+
406
+ if not chain_id:
407
+ return redemption_records
408
+
409
+ redemption_records = [rr for rr in redemption_records if rr["host_zone_id"] == chain_id]
410
+ return redemption_records
411
+
412
+
384
413
  def get_lsm_deposit_records(chain_id: str) -> List[Dict[str, Any]]:
385
414
  """
386
415
  Queries all the LSM deposit records
@@ -811,64 +840,67 @@ def generate_vesting_account(
811
840
 
812
841
 
813
842
  def get_block_height_in_past(
814
- time_in_past: datetime.timedelta = datetime.timedelta(hours=24),
815
- max_acceptable_error_seconds: int = 60 * 5,
843
+ target_time: Optional[datetime.datetime] = None,
844
+ target_time_offset: Optional[datetime.timedelta] = None,
845
+ error_tolerance: datetime.timedelta = datetime.timedelta(minutes=5),
816
846
  seconds_per_block: int = 6,
817
- prev_time: Optional[str] = None,
818
- verbose: bool = False,
819
847
  rpc_endpoint: str = config.stride.rpc_endpoint,
820
848
  ) -> int:
821
849
  """
822
850
  Gets the block height at a specified time by binary searching at different heights
823
851
 
824
852
  Args:
825
- time_in_past: target time to search for
826
- max_acceptable_error_seconds: max time delta before the height is considered find
853
+ target_time (datetime): the target time to search for
854
+ (e.g. search for the block at time 2024-01-01 01:00:00 UTC)
855
+ target_time_offset (timedelta): the target time to search for,
856
+ defined as an offset from the current time
857
+ (e.g. search for the block 24 hours ago)
858
+ error_tolerance (timedelta): max time delta before the height is considered found
827
859
  seconds_per_block: block time to use for the estimation start
828
- prev_time: Optional time to start the search at (instead of using time_in_past)
829
860
 
830
861
  Returns:
831
862
  The block height at the specified time
832
863
  """
864
+ # Get the current time and latest block height
865
+ current_time = datetime.datetime.now(tz=datetime.timezone.utc)
833
866
  latest_block = get_latest_block(rpc_endpoint=rpc_endpoint)
834
867
 
835
- # set parameters so we can search for the block height
836
- blocks_per_day = (24 * 60 * 60) // seconds_per_block
837
- if prev_time is not None:
838
- if type(prev_time) is str:
839
- prev_time: datetime.datetime = pd.to_datetime(prev_time) # type: ignore
840
- ts = (datetime.datetime.utcnow() - cast(datetime.datetime, prev_time)).total_seconds()
841
- num_days_back = ts / (60 * 60 * 24)
842
- time_in_past = datetime.timedelta(seconds=ts)
868
+ # Depending on whether the target time or target_time_offset was used,
869
+ # calculate the parameter that was not provided
870
+ if target_time and not target_time_offset:
871
+ target_time_offset = current_time - target_time
872
+ elif target_time_offset and not target_time:
873
+ target_time = current_time - target_time_offset
843
874
  else:
844
- num_days_back = time_in_past.total_seconds() / (60 * 60 * 24)
845
- blocks_in_past = int(num_days_back * blocks_per_day)
846
- time_estimate_error = float(100000)
847
-
848
- current_time = datetime.datetime.utcnow()
849
-
850
- # do an iterative search to find the block
851
- while abs(time_estimate_error) > max_acceptable_error_seconds:
852
- backwards_looking_height = latest_block - blocks_in_past
853
- backwards_looking_time = get_block_time(backwards_looking_height, rpc_endpoint=rpc_endpoint)
854
- time_estimate_error = (current_time - backwards_looking_time - time_in_past).total_seconds()
855
- error_in_blocks = time_estimate_error / seconds_per_block
856
- blocks_in_past -= int(error_in_blocks)
857
- if verbose:
858
- print("\tEstimated block: {}".format(backwards_looking_height))
859
- print("\tEstimated time: {}".format(backwards_looking_time))
860
- print("\tError in blocks: {}".format(error_in_blocks))
861
- print("\tTotal Seconds: {}".format(time_estimate_error))
862
- print("\t-------------------------")
863
- block_last_day = latest_block - blocks_in_past
864
- return block_last_day
865
-
866
-
867
- def get_block_height_one_day_ago(verbose: bool = False) -> int:
875
+ raise ValueError("Either target time or target time offset must be specified (but not both)")
876
+
877
+ # Using the target offset, determine the block delta to start our search, based
878
+ # on the estimated block time
879
+ blocks_in_past = int(target_time_offset.total_seconds() / seconds_per_block)
880
+ estimated_height = latest_block - blocks_in_past
881
+
882
+ # Continually loop over the following until we find a block that lines up with
883
+ # our target time (within our margin of error)
884
+ # - Guess a height
885
+ # - Find the time at that height
886
+ # - Check if the time is close enough to our target
887
+ # - Refine our block height guess and repeat
888
+ while True:
889
+ estimated_time = get_block_time(estimated_height, rpc_endpoint=rpc_endpoint)
890
+
891
+ time_error = target_time.timestamp() - estimated_time.timestamp()
892
+ if abs(time_error) < error_tolerance.total_seconds():
893
+ return estimated_height
894
+
895
+ blocks_correction = int(time_error / seconds_per_block)
896
+ estimated_height += blocks_correction
897
+
898
+
899
+ def get_block_height_one_day_ago() -> int:
868
900
  """
869
901
  Get the block height from 1 day ago
870
902
  """
871
- return get_block_height_in_past(datetime.timedelta(hours=24), verbose=verbose)
903
+ return get_block_height_in_past(target_time_offset=datetime.timedelta(hours=24))
872
904
 
873
905
 
874
906
  def get_sttoken_apr(token: str) -> Optional[float]:
@@ -1187,3 +1219,31 @@ def get_empirical_annual_fees(ticker: str) -> float:
1187
1219
  stride_upstash.get("sheet_STRD_STAKING_ST{}_DOLLARS".format(ticker.upper()), db_name="frontend") or "0"
1188
1220
  )
1189
1221
  return float(cast(str, empirical_dollar_fees))
1222
+
1223
+
1224
+ def query_evm_method(method: str, params: list[str], rpc_endpoint: str) -> str:
1225
+ """
1226
+ Generic querier for evm contract calls
1227
+ Returns a hex endcoded response specific to the contract call
1228
+ """
1229
+ headers = {"Content-Type": "application/json"}
1230
+ payload = {
1231
+ "jsonrpc": "2.0",
1232
+ "method": method,
1233
+ "params": params,
1234
+ "id": 1,
1235
+ }
1236
+
1237
+ response = request(rpc_endpoint, headers=headers, data=json.dumps(payload))
1238
+ return response["result"]
1239
+
1240
+
1241
+ def get_evm_balance(address: str, rpc_endpoint: str) -> int:
1242
+ """
1243
+ Queries the native token balance of an evm address
1244
+ Address should be a hex address with an 0x prefix
1245
+ """
1246
+ params = [address, "latest"]
1247
+ balance_raw = query_evm_method("eth_getBalance", params=params, rpc_endpoint=rpc_endpoint)
1248
+ balance = int(balance_raw, 16)
1249
+ return balance
File without changes