intentkit 0.6.21.dev3__py3-none-any.whl → 0.6.22__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.
- intentkit/__init__.py +1 -1
- intentkit/skills/dexscreener/README.md +154 -0
- intentkit/skills/dexscreener/__init__.py +9 -0
- intentkit/skills/dexscreener/base.py +1 -4
- intentkit/skills/dexscreener/get_pair_info.py +159 -0
- intentkit/skills/dexscreener/get_token_pairs.py +166 -0
- intentkit/skills/dexscreener/get_tokens_info.py +213 -0
- intentkit/skills/dexscreener/schema.json +46 -1
- intentkit/skills/dexscreener/search_token.py +56 -193
- intentkit/skills/dexscreener/utils.py +419 -0
- intentkit/skills/xmtp/base.py +60 -1
- intentkit/skills/xmtp/price.py +11 -7
- intentkit/skills/xmtp/swap.py +16 -17
- intentkit/skills/xmtp/transfer.py +9 -22
- {intentkit-0.6.21.dev3.dist-info → intentkit-0.6.22.dist-info}/METADATA +1 -1
- {intentkit-0.6.21.dev3.dist-info → intentkit-0.6.22.dist-info}/RECORD +18 -13
- {intentkit-0.6.21.dev3.dist-info → intentkit-0.6.22.dist-info}/WHEEL +0 -0
- {intentkit-0.6.21.dev3.dist-info → intentkit-0.6.22.dist-info}/licenses/LICENSE +0 -0
|
@@ -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)
|
intentkit/skills/xmtp/base.py
CHANGED
|
@@ -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]
|
intentkit/skills/xmtp/price.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
intentkit/skills/xmtp/swap.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
"
|
|
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
|
-
#
|
|
91
|
-
|
|
92
|
-
"
|
|
93
|
-
"base-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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"
|
|
99
|
+
f"Swap only supported on {', '.join(supported_networks)}. Current: {agent.network_id}"
|
|
99
100
|
)
|
|
100
101
|
|
|
101
|
-
|
|
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
|
|
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 (
|
|
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', '
|
|
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
|
|
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.
|
|
33
|
-
|
|
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
|
-
#
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: intentkit
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.22
|
|
4
4
|
Summary: Intent-based AI Agent Platform - Core Package
|
|
5
5
|
Project-URL: Homepage, https://github.com/crestalnetwork/intentkit
|
|
6
6
|
Project-URL: Repository, https://github.com/crestalnetwork/intentkit
|