Coverage for yield_analysis_sdk\subgraph.py: 79%

47 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-07-17 20:34 +0800

1from typing import Any, Dict, List 

2 

3from requests import post 

4 

5from .exceptions import ConfigurationError, ConnectionError 

6from .type import Chain, SharePriceHistory 

7from .validators import validate_address_value 

8 

9SUBGRAPH_QUERY_URLS = { 

10 Chain.BASE: "https://gateway.thegraph.com/api/subgraphs/id/46pQKDXgcredBSK9cbGU8qEaPEpEZgQ72hSAkpWnKinJ", 

11} 

12 

13daily_share_price_query = """ 

14query DailyPriceHistory($vault_addresses: [Bytes!], $length: Int!) { 

15 vaultStats_collection( 

16 interval: day 

17 orderBy: timestamp 

18 orderDirection: desc 

19 first: $length 

20 where: { 

21 vault_: { 

22 address_in: $vault_addresses 

23 } 

24 } 

25 ) { 

26 timestamp 

27 pricePerShare 

28 vault { 

29 address 

30 name 

31 decimals 

32 } 

33 } 

34} 

35""" 

36 

37 

38def _format_vault_addresses(addresses: List[str]) -> List[str]: 

39 """Format vault addresses to lowercase for GraphQL compatibility""" 

40 return [validate_address_value(addr) for addr in addresses] 

41 

42 

43def _send_graphql_query_to_subgraph( 

44 chain: Chain, 

45 query: str, 

46 variables: Dict[str, Any], 

47 api_key: str, 

48) -> Any: 

49 headers = {"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"} 

50 

51 # Prepare the request payload 

52 payload = {"query": query, "variables": variables} 

53 

54 # Send the GraphQL request to the Subgraph 

55 response = post(SUBGRAPH_QUERY_URLS[chain], headers=headers, json=payload) 

56 

57 # Check if the request was successful 

58 if response.status_code == 200: 

59 result = response.json() 

60 if "errors" in result: 

61 raise ConnectionError(f"GraphQL errors: {result['errors']}") 

62 else: 

63 raise ConnectionError(f"HTTP Error {response.status_code}: {response.text}") 

64 

65 return result 

66 

67 

68def _format_price_history_response(res: dict, underlying_asset_decimals: int) -> List[SharePriceHistory]: 

69 if not res or "data" not in res or not res["data"]["vaultStats_collection"]: 

70 return [] 

71 

72 history_by_vault = {} 

73 

74 for entry in res["data"]["vaultStats_collection"]: 

75 vault_address = entry["vault"]["address"] 

76 vault_name = entry["vault"]["name"] 

77 vault_decimals = int(entry["vault"]["decimals"]) 

78 timestamp = ( 

79 int(entry["timestamp"]) // 1000000 

80 ) # Convert microseconds to seconds 

81 decimals_multiplier: float = 10 ** (vault_decimals - underlying_asset_decimals) 

82 price_per_share = float(entry["pricePerShare"]) * decimals_multiplier 

83 

84 if vault_address not in history_by_vault: 

85 history_by_vault[vault_address] = { 

86 "vault_name": vault_name, 

87 "vault_address": vault_address, 

88 "price_history": [], 

89 } 

90 

91 history_by_vault[vault_address]["price_history"].append( 

92 (timestamp, price_per_share) 

93 ) 

94 

95 # Sort price history by timestamp (oldest first) 

96 for vault_address in history_by_vault: 

97 history_by_vault[vault_address]["price_history"].sort(key=lambda x: x[0]) 

98 

99 # Convert to SharePriceHistory objects 

100 result = [] 

101 for vault_data in history_by_vault.values(): 

102 share_price_history = SharePriceHistory( 

103 vault_name=vault_data["vault_name"], 

104 vault_address=vault_data["vault_address"], 

105 price_history=vault_data["price_history"], 

106 ) 

107 result.append(share_price_history) 

108 

109 return result 

110 

111 

112def get_daily_share_price_history_from_subgraph( 

113 chain: Chain, vault_addresses: List[str], underlying_asset_decimals: int, length: int, api_key: str 

114) -> List[SharePriceHistory]: 

115 """ 

116 Get the daily share price history from the subgraph for a list of vault addresses. 

117 

118 Args: 

119 chain: The blockchain chain to query. 

120 vault_addresses: A list of vault addresses to query. 

121 length: The number of days to query. 

122 api_key: The API key for the subgraph. 

123 """ 

124 if not api_key: 

125 raise ConfigurationError("SUBGRAPH_API_KEY is required") 

126 

127 formatted_addresses = _format_vault_addresses(vault_addresses) 

128 

129 variables = { 

130 "vault_addresses": formatted_addresses, 

131 "length": length * len(formatted_addresses), 

132 } 

133 

134 res: dict = _send_graphql_query_to_subgraph( 

135 chain, daily_share_price_query, variables, api_key 

136 ) 

137 return _format_price_history_response(res, underlying_asset_decimals)