intentkit 0.6.21.dev2__py3-none-any.whl → 0.6.22.dev1__py3-none-any.whl

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.

Potentially problematic release.


This version of intentkit might be problematic. Click here for more details.

@@ -0,0 +1,419 @@
1
+ """
2
+ Utility functions and constants for DexScreener skills.
3
+ """
4
+
5
+ import json
6
+ import logging
7
+ from enum import Enum
8
+ from typing import Any, Callable, Dict, List, Optional
9
+
10
+ from pydantic import ValidationError
11
+
12
+ from intentkit.skills.dexscreener.model.search_token_response import PairModel
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # API Base URL
17
+ DEXSCREENER_BASE_URL = "https://api.dexscreener.com"
18
+
19
+ # API Endpoints
20
+ API_ENDPOINTS = {
21
+ "search": "/latest/dex/search",
22
+ "pairs": "/latest/dex/pairs",
23
+ "token_pairs": "/token-pairs/v1",
24
+ "tokens": "/tokens/v1",
25
+ "token_profiles": "/token-profiles/latest/v1",
26
+ "token_boosts_latest": "/token-boosts/latest/v1",
27
+ "token_boosts_top": "/token-boosts/top/v1",
28
+ "orders": "/orders/v1",
29
+ }
30
+
31
+ # Rate Limits (requests per minute)
32
+ RATE_LIMITS = {
33
+ "search": 300,
34
+ "pairs": 300,
35
+ "token_pairs": 300,
36
+ "tokens": 300,
37
+ "token_profiles": 60,
38
+ "token_boosts": 60,
39
+ "orders": 60,
40
+ }
41
+
42
+ # Limits
43
+ MAX_SEARCH_RESULTS = 25
44
+ MAX_TOKENS_BATCH = 30
45
+
46
+ # Common disclaimer for search results
47
+ SEARCH_DISCLAIMER = {
48
+ "disclaimer": (
49
+ "Search results may include unofficial, duplicate, or potentially malicious tokens. "
50
+ "If multiple unrelated tokens share a similar name or ticker, ask the user for the exact token address. "
51
+ "If the correct token is not found, re-run the tool using the provided address. "
52
+ "Also advise the user to verify the token's legitimacy via its official social links included in the result."
53
+ )
54
+ }
55
+
56
+
57
+ # Query Types
58
+ class QueryType(str, Enum):
59
+ TEXT = "TEXT"
60
+ TICKER = "TICKER"
61
+ ADDRESS = "ADDRESS"
62
+
63
+
64
+ # Sort Options
65
+ class SortBy(str, Enum):
66
+ LIQUIDITY = "liquidity"
67
+ VOLUME = "volume"
68
+
69
+
70
+ # Volume Timeframes
71
+ class VolumeTimeframe(str, Enum):
72
+ FIVE_MINUTES = "5_minutes"
73
+ ONE_HOUR = "1_hour"
74
+ SIX_HOUR = "6_hour"
75
+ TWENTY_FOUR_HOUR = "24_hour"
76
+
77
+
78
+ # Supported Chain IDs
79
+ SUPPORTED_CHAINS = [
80
+ "ethereum",
81
+ "bsc",
82
+ "polygon",
83
+ "avalanche",
84
+ "fantom",
85
+ "cronos",
86
+ "arbitrum",
87
+ "optimism",
88
+ "base",
89
+ "solana",
90
+ "sui",
91
+ "tron",
92
+ "ton",
93
+ ]
94
+
95
+
96
+ def determine_query_type(query: str) -> QueryType:
97
+ """
98
+ Determine whether the query is a TEXT, TICKER, or ADDRESS.
99
+
100
+ Args:
101
+ query: The search query string
102
+
103
+ Returns:
104
+ QueryType enum value
105
+ """
106
+ if query.startswith("0x"):
107
+ return QueryType.ADDRESS
108
+ if query.startswith("$"):
109
+ return QueryType.TICKER
110
+ return QueryType.TEXT
111
+
112
+
113
+ def get_liquidity_value(pair: PairModel) -> float:
114
+ """
115
+ Extract liquidity USD value from a pair, defaulting to 0.0 if not available.
116
+
117
+ Args:
118
+ pair: PairModel instance
119
+
120
+ Returns:
121
+ Liquidity value in USD as float
122
+ """
123
+ return (
124
+ pair.liquidity.usd if pair.liquidity and pair.liquidity.usd is not None else 0.0
125
+ )
126
+
127
+
128
+ def get_volume_value(
129
+ pair: PairModel, timeframe: VolumeTimeframe = VolumeTimeframe.TWENTY_FOUR_HOUR
130
+ ) -> float:
131
+ """
132
+ Extract volume value from a pair for the specified timeframe.
133
+
134
+ Args:
135
+ pair: PairModel instance
136
+ timeframe: VolumeTimeframe enum value
137
+
138
+ Returns:
139
+ Volume value as float
140
+ """
141
+ if not pair.volume:
142
+ return 0.0
143
+
144
+ volume_map = {
145
+ VolumeTimeframe.FIVE_MINUTES: pair.volume.m5,
146
+ VolumeTimeframe.ONE_HOUR: pair.volume.h1,
147
+ VolumeTimeframe.SIX_HOUR: pair.volume.h6,
148
+ VolumeTimeframe.TWENTY_FOUR_HOUR: pair.volume.h24,
149
+ }
150
+
151
+ return volume_map.get(timeframe, 0.0) or 0.0
152
+
153
+
154
+ def get_sort_function(
155
+ sort_by: SortBy,
156
+ volume_timeframe: VolumeTimeframe = VolumeTimeframe.TWENTY_FOUR_HOUR,
157
+ ) -> Callable[[PairModel], float]:
158
+ """
159
+ Get the appropriate sorting function based on sort criteria.
160
+
161
+ Args:
162
+ sort_by: SortBy enum value
163
+ volume_timeframe: VolumeTimeframe enum value (used when sorting by volume)
164
+
165
+ Returns:
166
+ Callable function that takes a PairModel and returns a float for sorting
167
+ """
168
+ if sort_by == SortBy.LIQUIDITY:
169
+ return get_liquidity_value
170
+ elif sort_by == SortBy.VOLUME:
171
+ return lambda pair: get_volume_value(pair, volume_timeframe)
172
+ else:
173
+ logger.warning(f"Invalid sort_by value '{sort_by}', defaulting to liquidity.")
174
+ return get_liquidity_value
175
+
176
+
177
+ def sort_pairs_by_criteria(
178
+ pairs: List[PairModel],
179
+ sort_by: SortBy = SortBy.LIQUIDITY,
180
+ volume_timeframe: VolumeTimeframe = VolumeTimeframe.TWENTY_FOUR_HOUR,
181
+ reverse: bool = True,
182
+ ) -> List[PairModel]:
183
+ """
184
+ Sort pairs by the specified criteria.
185
+
186
+ Args:
187
+ pairs: List of PairModel instances to sort
188
+ sort_by: Sorting criteria (liquidity or volume)
189
+ volume_timeframe: Timeframe for volume sorting
190
+ reverse: Sort in descending order if True
191
+
192
+ Returns:
193
+ Sorted list of PairModel instances
194
+ """
195
+ try:
196
+ sort_func = get_sort_function(sort_by, volume_timeframe)
197
+ return sorted(pairs, key=sort_func, reverse=reverse)
198
+ except Exception as e:
199
+ logger.error(f"Failed to sort pairs: {e}", exc_info=True)
200
+ return pairs # Return original list if sorting fails
201
+
202
+
203
+ def filter_ticker_pairs(pairs: List[PairModel], target_ticker: str) -> List[PairModel]:
204
+ """
205
+ Filter pairs to only include those where base token symbol matches target ticker.
206
+
207
+ Args:
208
+ pairs: List of PairModel instances
209
+ target_ticker: Target ticker symbol (case-insensitive)
210
+
211
+ Returns:
212
+ Filtered list of PairModel instances
213
+ """
214
+ target_ticker_upper = target_ticker.upper()
215
+ return [
216
+ p
217
+ for p in pairs
218
+ if p.baseToken
219
+ and p.baseToken.symbol
220
+ and p.baseToken.symbol.upper() == target_ticker_upper
221
+ ]
222
+
223
+
224
+ def filter_address_pairs(
225
+ pairs: List[PairModel], target_address: str
226
+ ) -> List[PairModel]:
227
+ """
228
+ Filter pairs to only include those matching the target address.
229
+ Checks pairAddress, baseToken.address, and quoteToken.address.
230
+
231
+ Args:
232
+ pairs: List of PairModel instances
233
+ target_address: Target address (case-insensitive)
234
+
235
+ Returns:
236
+ Filtered list of PairModel instances
237
+ """
238
+ target_address_lower = target_address.lower()
239
+ return [
240
+ p
241
+ for p in pairs
242
+ if (p.pairAddress and p.pairAddress.lower() == target_address_lower)
243
+ or (
244
+ p.baseToken
245
+ and p.baseToken.address
246
+ and p.baseToken.address.lower() == target_address_lower
247
+ )
248
+ or (
249
+ p.quoteToken
250
+ and p.quoteToken.address
251
+ and p.quoteToken.address.lower() == target_address_lower
252
+ )
253
+ ]
254
+
255
+
256
+ def create_error_response(
257
+ error_type: str,
258
+ message: str,
259
+ details: Optional[str] = None,
260
+ additional_data: Optional[Dict[str, Any]] = None,
261
+ ) -> str:
262
+ """
263
+ Create a standardized error response in JSON format.
264
+
265
+ Args:
266
+ error_type: Type/category of error
267
+ message: Human-readable error message
268
+ details: Optional additional details about the error
269
+ additional_data: Optional dictionary of additional data to include
270
+
271
+ Returns:
272
+ JSON string containing error information
273
+ """
274
+ response = {
275
+ "error": message,
276
+ "error_type": error_type,
277
+ }
278
+
279
+ if details:
280
+ response["details"] = details
281
+
282
+ if additional_data:
283
+ response.update(additional_data)
284
+
285
+ return json.dumps(response, indent=2)
286
+
287
+
288
+ def create_no_results_response(
289
+ query_info: str,
290
+ reason: str = "no results found",
291
+ additional_data: Optional[Dict[str, Any]] = None,
292
+ ) -> str:
293
+ """
294
+ Create a standardized "no results found" response.
295
+
296
+ Args:
297
+ query_info: Information about the query that was performed
298
+ reason: Reason why no results were found
299
+ additional_data: Optional additional data to include
300
+
301
+ Returns:
302
+ JSON string containing no results information
303
+ """
304
+ response = {
305
+ "message": f"No results found for the query. Reason: {reason}.",
306
+ "query_info": query_info,
307
+ "pairs": [],
308
+ }
309
+
310
+ if additional_data:
311
+ response.update(additional_data)
312
+
313
+ return json.dumps(response, indent=2)
314
+
315
+
316
+ def handle_validation_error(
317
+ error: ValidationError, query_info: str, data_length: Optional[int] = None
318
+ ) -> str:
319
+ """
320
+ Handle validation errors in a standardized way.
321
+
322
+ Args:
323
+ error: The ValidationError that occurred
324
+ query_info: Information about the query being processed
325
+ data_length: Optional length of the data that failed validation
326
+
327
+ Returns:
328
+ JSON error response string
329
+ """
330
+ log_message = f"Failed to validate DexScreener response structure for {query_info}. Error: {error}"
331
+ if data_length:
332
+ log_message += f". Raw data length: {data_length}"
333
+
334
+ logger.error(log_message, exc_info=True)
335
+
336
+ return create_error_response(
337
+ error_type="validation_error",
338
+ message="Failed to parse successful DexScreener API response",
339
+ details=str(error.errors()),
340
+ additional_data={"query_info": query_info},
341
+ )
342
+
343
+
344
+ def truncate_large_fields(
345
+ data: Dict[str, Any], max_length: int = 500
346
+ ) -> Dict[str, Any]:
347
+ """
348
+ Truncate large string fields in error response data to avoid overwhelming the LLM.
349
+
350
+ Args:
351
+ data: Dictionary potentially containing large string fields
352
+ max_length: Maximum length for string fields before truncation
353
+
354
+ Returns:
355
+ Dictionary with truncated fields
356
+ """
357
+ truncated = data.copy()
358
+
359
+ for key in ["details", "response_body"]:
360
+ if isinstance(truncated.get(key), str) and len(truncated[key]) > max_length:
361
+ truncated[key] = truncated[key][:max_length] + "... (truncated)"
362
+
363
+ return truncated
364
+
365
+
366
+ def group_pairs_by_token(pairs: List[PairModel]) -> Dict[str, List[PairModel]]:
367
+ """
368
+ Group pairs by token address for better organization in multi-token responses.
369
+
370
+ Args:
371
+ pairs: List of PairModel instances
372
+
373
+ Returns:
374
+ Dictionary mapping lowercase token addresses to lists of pairs
375
+ """
376
+ tokens_data = {}
377
+
378
+ for pair in pairs:
379
+ # Group by base token address
380
+ if pair.baseToken and pair.baseToken.address:
381
+ base_addr = pair.baseToken.address.lower()
382
+ if base_addr not in tokens_data:
383
+ tokens_data[base_addr] = []
384
+ tokens_data[base_addr].append(pair)
385
+
386
+ # Group by quote token address
387
+ if pair.quoteToken and pair.quoteToken.address:
388
+ quote_addr = pair.quoteToken.address.lower()
389
+ if quote_addr not in tokens_data:
390
+ tokens_data[quote_addr] = []
391
+ tokens_data[quote_addr].append(pair)
392
+
393
+ return tokens_data
394
+
395
+
396
+ def validate_chain_id(chain_id: str) -> bool:
397
+ """
398
+ Validate if the chain ID is supported.
399
+
400
+ Args:
401
+ chain_id: Chain ID to validate
402
+
403
+ Returns:
404
+ True if chain ID is supported, False otherwise
405
+ """
406
+ return chain_id.lower() in SUPPORTED_CHAINS
407
+
408
+
409
+ def format_success_response(data: Dict[str, Any]) -> str:
410
+ """
411
+ Format a successful response as JSON string.
412
+
413
+ Args:
414
+ data: Response data dictionary
415
+
416
+ Returns:
417
+ JSON formatted string
418
+ """
419
+ return json.dumps(data, indent=2)
@@ -1,4 +1,4 @@
1
- from typing import Literal
1
+ from typing import Dict, Literal
2
2
 
3
3
  from intentkit.skills.base import IntentKitSkill
4
4
 
@@ -9,7 +9,66 @@ class XmtpBaseTool(IntentKitSkill):
9
9
  # Set response format to content_and_artifact for returning tuple
10
10
  response_format: Literal["content", "content_and_artifact"] = "content_and_artifact"
11
11
 
12
+ # ChainId mapping for XMTP wallet_sendCalls (mainnet only)
13
+ CHAIN_ID_HEX_BY_NETWORK: Dict[str, str] = {
14
+ "ethereum-mainnet": "0x1", # 1
15
+ "base-mainnet": "0x2105", # 8453
16
+ "arbitrum-mainnet": "0xA4B1", # 42161
17
+ "optimism-mainnet": "0xA", # 10
18
+ }
19
+
20
+ # CDP network mapping for swap quote API (mainnet only)
21
+ NETWORK_FOR_CDP_MAPPING: Dict[str, str] = {
22
+ "ethereum-mainnet": "ethereum",
23
+ "base-mainnet": "base",
24
+ "arbitrum-mainnet": "arbitrum",
25
+ "optimism-mainnet": "optimism",
26
+ }
27
+
12
28
  @property
13
29
  def category(self) -> str:
14
30
  """Return the skill category."""
15
31
  return "xmtp"
32
+
33
+ def validate_network_and_get_chain_id(
34
+ self, network_id: str, skill_name: str
35
+ ) -> str:
36
+ """Validate network and return chain ID hex.
37
+
38
+ Args:
39
+ network_id: The network ID to validate
40
+ skill_name: The name of the skill for error messages
41
+
42
+ Returns:
43
+ The hex chain ID for the network
44
+
45
+ Raises:
46
+ ValueError: If the network is not supported
47
+ """
48
+ if network_id not in self.CHAIN_ID_HEX_BY_NETWORK:
49
+ supported_networks = ", ".join(self.CHAIN_ID_HEX_BY_NETWORK.keys())
50
+ raise ValueError(
51
+ f"XMTP {skill_name} supports the following networks: {supported_networks}. "
52
+ f"Current agent network: {network_id}"
53
+ )
54
+ return self.CHAIN_ID_HEX_BY_NETWORK[network_id]
55
+
56
+ def get_cdp_network(self, network_id: str) -> str:
57
+ """Get CDP network name for the given network ID.
58
+
59
+ Args:
60
+ network_id: The network ID
61
+
62
+ Returns:
63
+ The CDP network name
64
+
65
+ Raises:
66
+ ValueError: If the network is not supported for CDP
67
+ """
68
+ if network_id not in self.NETWORK_FOR_CDP_MAPPING:
69
+ supported_networks = ", ".join(self.NETWORK_FOR_CDP_MAPPING.keys())
70
+ raise ValueError(
71
+ f"CDP swap does not support network: {network_id}. "
72
+ f"Supported networks: {supported_networks}"
73
+ )
74
+ return self.NETWORK_FOR_CDP_MAPPING[network_id]
@@ -21,7 +21,7 @@ class XmtpGetSwapPrice(XmtpBaseTool):
21
21
  """Skill for fetching indicative swap price using CDP SDK."""
22
22
 
23
23
  name: str = "xmtp_get_swap_price"
24
- description: str = "Get an indicative swap price/quote for token pair and amount on Base networks using CDP."
24
+ description: str = "Get an indicative swap price/quote for token pair and amount on Ethereum, Base, Arbitrum, and Optimism mainnet networks using CDP."
25
25
  response_format: Literal["content", "content_and_artifact"] = "content"
26
26
  args_schema: Type[BaseModel] = SwapPriceInput
27
27
 
@@ -35,15 +35,19 @@ class XmtpGetSwapPrice(XmtpBaseTool):
35
35
  context = self.get_context()
36
36
  agent = context.agent
37
37
 
38
- if agent.network_id not in ("base-mainnet", "base-sepolia"):
38
+ # Only support mainnet networks for price and swap
39
+ supported_networks = [
40
+ "ethereum-mainnet",
41
+ "base-mainnet",
42
+ "arbitrum-mainnet",
43
+ "optimism-mainnet",
44
+ ]
45
+ if agent.network_id not in supported_networks:
39
46
  raise ValueError(
40
- f"Swap price only supported on base-mainnet or base-sepolia. Current: {agent.network_id}"
47
+ f"Swap price only supported on {', '.join(supported_networks)}. Current: {agent.network_id}"
41
48
  )
42
49
 
43
- network_for_cdp = {
44
- "base-mainnet": "base",
45
- "base-sepolia": "base-sepolia",
46
- }[agent.network_id]
50
+ network_for_cdp = self.get_cdp_network(agent.network_id)
47
51
 
48
52
  cdp_client = get_origin_cdp_client(self.skill_store)
49
53
  # Note: Don't use async with context manager as get_origin_cdp_client returns a managed global client
@@ -33,14 +33,14 @@ class XmtpSwap(XmtpBaseTool):
33
33
 
34
34
  Generates a wallet_sendCalls transaction request to perform a token swap.
35
35
  May include an ERC20 approval call followed by the router swap call.
36
- Supports Base mainnet and Base Sepolia testnet.
36
+ Supports Ethereum, Polygon, Base, Arbitrum, and Optimism networks (both mainnet and testnet).
37
37
  """
38
38
 
39
39
  name: str = "xmtp_swap"
40
40
  description: str = (
41
- "Create an XMTP transaction request for swapping tokens on Base using CDP swap quote. "
41
+ "Create an XMTP transaction request for swapping tokens using CDP swap quote. "
42
42
  "Returns a wallet_sendCalls payload that can include an optional approval call and the swap call. "
43
- "Only supports base-mainnet and base-sepolia."
43
+ "Supports Ethereum, Base, Arbitrum, and Optimism mainnet networks."
44
44
  )
45
45
  args_schema: Type[BaseModel] = SwapInput
46
46
 
@@ -87,26 +87,25 @@ class XmtpSwap(XmtpBaseTool):
87
87
  context = self.get_context()
88
88
  agent = context.agent
89
89
 
90
- # ChainId mapping for XMTP wallet_sendCalls
91
- chain_id_hex_by_network = {
92
- "base-mainnet": "0x2105", # 8453
93
- "base-sepolia": "0x14A34", # 84532
94
- }
95
-
96
- if agent.network_id not in chain_id_hex_by_network:
90
+ # Only support mainnet networks for swap
91
+ supported_networks = [
92
+ "ethereum-mainnet",
93
+ "base-mainnet",
94
+ "arbitrum-mainnet",
95
+ "optimism-mainnet",
96
+ ]
97
+ if agent.network_id not in supported_networks:
97
98
  raise ValueError(
98
- f"XMTP swap only supports base-mainnet or base-sepolia. Current agent network: {agent.network_id}"
99
+ f"Swap only supported on {', '.join(supported_networks)}. Current: {agent.network_id}"
99
100
  )
100
101
 
101
- chain_id_hex = chain_id_hex_by_network[agent.network_id]
102
+ # Validate network and get chain ID
103
+ chain_id_hex = self.validate_network_and_get_chain_id(agent.network_id, "swap")
102
104
 
103
- # CDP network mapping for swap quote API
105
+ # Get CDP network name
104
106
  # Reference: CDP SDK examples for swap quote and price
105
107
  # https://github.com/coinbase/cdp-sdk/blob/main/examples/python/evm/swaps/create_swap_quote.py
106
- network_for_cdp = {
107
- "base-mainnet": "base",
108
- "base-sepolia": "base-sepolia",
109
- }[agent.network_id]
108
+ network_for_cdp = self.get_cdp_network(agent.network_id)
110
109
 
111
110
  # Get CDP client from global origin helper (server-side credentials)
112
111
  cdp_client = get_origin_cdp_client(self.skill_store)
@@ -13,9 +13,9 @@ class TransferInput(BaseModel):
13
13
  from_address: str = Field(description="The sender address for the transfer")
14
14
  to_address: str = Field(description="The recipient address for the transfer")
15
15
  amount: str = Field(
16
- description="The amount to transfer (as string to handle large numbers)"
16
+ description="The amount to transfer in human-readable format (e.g., '1.5' for 1.5 ETH, '100' for 100 USDC). Do NOT multiply by token decimals."
17
17
  )
18
- currency: str = Field(description="Currency symbol (e.g., 'ETH', 'USDC', 'DAI')")
18
+ currency: str = Field(description="Currency symbol (e.g., 'ETH', 'USDC', 'NATION')")
19
19
  token_contract_address: Optional[str] = Field(
20
20
  default=None,
21
21
  description="Token contract address for ERC20 transfers. Leave empty for ETH transfers.",
@@ -26,14 +26,10 @@ class XmtpTransfer(XmtpBaseTool):
26
26
  """Skill for creating XMTP transfer transactions."""
27
27
 
28
28
  name: str = "xmtp_transfer"
29
- description: str = """Create an XMTP transaction request for transferring ETH or ERC20 tokens on Base mainnet.
30
-
29
+ description: str = """Create an XMTP transaction request for transferring ETH or ERC20 tokens.
31
30
  This skill generates a wallet_sendCalls transaction request according to XMTP protocol
32
- that can be sent to users for signing. The transaction can transfer:
33
- - ETH (when token_contract_address is not provided)
34
- - ERC20 tokens (when token_contract_address is provided)
35
-
36
- Only supports Base mainnet network.
31
+ that can be sent to users for signing.
32
+ Supports Ethereum, Polygon, Base, Arbitrum, and Optimism networks (both mainnet and testnet).
37
33
  """
38
34
  args_schema: Type[BaseModel] = TransferInput
39
35
 
@@ -61,19 +57,10 @@ class XmtpTransfer(XmtpBaseTool):
61
57
  context = self.get_context()
62
58
  agent = context.agent
63
59
 
64
- # ChainId mapping for XMTP wallet_sendCalls
65
- chain_id_hex_by_network = {
66
- "base-mainnet": "0x2105", # 8453
67
- "base-sepolia": "0x14A34", # 84532
68
- }
69
-
70
- if agent.network_id not in chain_id_hex_by_network:
71
- raise ValueError(
72
- f"XMTP transfer only supports base-mainnet or base-sepolia network. "
73
- f"Current agent network: {agent.network_id}"
74
- )
75
-
76
- chain_id_hex = chain_id_hex_by_network[agent.network_id]
60
+ # Validate network and get chain ID
61
+ chain_id_hex = self.validate_network_and_get_chain_id(
62
+ agent.network_id, "transfer"
63
+ )
77
64
 
78
65
  # Validate token contract and get decimals
79
66
  if token_contract_address:
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: intentkit
3
- Version: 0.6.21.dev2
3
+ Version: 0.6.22.dev1
4
4
  Summary: Intent-based AI Agent Platform - Core Package
5
- Project-URL: Homepage, https://github.com/crestal-network/intentkit
6
- Project-URL: Repository, https://github.com/crestal-network/intentkit
7
- Project-URL: Documentation, https://github.com/crestal-network/intentkit/tree/main/docs
8
- Project-URL: Bug Tracker, https://github.com/crestal-network/intentkit/issues
5
+ Project-URL: Homepage, https://github.com/crestalnetwork/intentkit
6
+ Project-URL: Repository, https://github.com/crestalnetwork/intentkit
7
+ Project-URL: Documentation, https://github.com/crestalnetwork/intentkit/tree/main/docs
8
+ Project-URL: Bug Tracker, https://github.com/crestalnetwork/intentkit/issues
9
9
  Author-email: hyacinthus <hyacinthus@gmail.com>
10
10
  License: MIT License
11
11