intentkit 0.7.5.dev25__py3-none-any.whl → 0.7.5.dev27__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 CHANGED
@@ -3,7 +3,7 @@
3
3
  A powerful platform for building AI agents with blockchain and cryptocurrency capabilities.
4
4
  """
5
5
 
6
- __version__ = "0.7.5-dev25"
6
+ __version__ = "0.7.5-dev27"
7
7
  __author__ = "hyacinthus"
8
8
  __email__ = "hyacinthus@gmail.com"
9
9
 
@@ -0,0 +1,208 @@
1
+ """Shared asset retrieval utilities for agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from decimal import Decimal
7
+ from typing import Optional
8
+
9
+ import httpx
10
+ from pydantic import BaseModel, Field
11
+ from web3 import Web3
12
+
13
+ from intentkit.clients.web3 import get_web3_client
14
+ from intentkit.config.config import config
15
+ from intentkit.core.agent import agent_store
16
+ from intentkit.models.agent import Agent
17
+ from intentkit.models.agent_data import AgentData
18
+ from intentkit.utils.error import IntentKitAPIError
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # USDC contract addresses for different networks
23
+ USDC_ADDRESSES = {
24
+ "base-mainnet": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
25
+ "ethereum-mainnet": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
26
+ "arbitrum-mainnet": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
27
+ "optimism-mainnet": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
28
+ "polygon-mainnet": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
29
+ }
30
+
31
+ # NATION token address for base-mainnet
32
+ NATION_ADDRESS = "0x2f74f818e81685c8086dd783837a4605a90474b8"
33
+
34
+
35
+ class Asset(BaseModel):
36
+ """Model for individual asset with symbol and balance."""
37
+
38
+ symbol: str = Field(description="Asset symbol (e.g., ETH, USDC, NATION)")
39
+ balance: Decimal = Field(description="Asset balance as decimal")
40
+
41
+
42
+ class AgentAssets(BaseModel):
43
+ """Simplified agent asset response with wallet net worth and tokens."""
44
+
45
+ net_worth: str = Field(description="Total wallet net worth in USD")
46
+ tokens: list[Asset] = Field(description="List of assets with symbol and balance")
47
+
48
+
49
+ async def _get_token_balance(
50
+ web3_client: Web3, wallet_address: str, token_address: str
51
+ ) -> Decimal:
52
+ """Get ERC-20 token balance for a wallet address."""
53
+ try:
54
+ # ERC-20 standard ABI for balanceOf and decimals
55
+ erc20_abi = [
56
+ {
57
+ "constant": True,
58
+ "inputs": [{"name": "_owner", "type": "address"}],
59
+ "name": "balanceOf",
60
+ "outputs": [{"name": "balance", "type": "uint256"}],
61
+ "type": "function",
62
+ },
63
+ {
64
+ "constant": True,
65
+ "inputs": [],
66
+ "name": "decimals",
67
+ "outputs": [{"name": "", "type": "uint8"}],
68
+ "type": "function",
69
+ },
70
+ ]
71
+
72
+ contract = web3_client.eth.contract(
73
+ address=web3_client.to_checksum_address(token_address), abi=erc20_abi
74
+ )
75
+
76
+ balance_wei = contract.functions.balanceOf(
77
+ web3_client.to_checksum_address(wallet_address)
78
+ ).call()
79
+ decimals = contract.functions.decimals().call()
80
+
81
+ # Convert from wei to token units using actual decimals
82
+ balance = Decimal(balance_wei) / Decimal(10**decimals)
83
+ return balance
84
+ except Exception as exc: # pragma: no cover - log path only
85
+ logger.error("Error getting token balance: %s", exc)
86
+ return Decimal("0")
87
+
88
+
89
+ async def _get_eth_balance(web3_client: Web3, wallet_address: str) -> Decimal:
90
+ """Get ETH balance for a wallet address."""
91
+ try:
92
+ balance_wei = web3_client.eth.get_balance(
93
+ web3_client.to_checksum_address(wallet_address)
94
+ )
95
+ balance = Decimal(balance_wei) / Decimal(10**18)
96
+ return balance
97
+ except Exception as exc: # pragma: no cover - log path only
98
+ logger.error("Error getting ETH balance: %s", exc)
99
+ return Decimal("0")
100
+
101
+
102
+ async def _get_wallet_net_worth(wallet_address: str) -> str:
103
+ """Get wallet net worth using Moralis API."""
104
+ try:
105
+ async with httpx.AsyncClient() as client:
106
+ url = (
107
+ "https://deep-index.moralis.io/api/v2.2/wallets/"
108
+ f"{wallet_address}/net-worth"
109
+ )
110
+ headers = {
111
+ "accept": "application/json",
112
+ "X-API-Key": config.moralis_api_key,
113
+ }
114
+ params = {
115
+ "exclude_spam": "true",
116
+ "exclude_unverified_contracts": "true",
117
+ "chains": ["eth", "base", "polygon", "arbitrum", "optimism"],
118
+ }
119
+
120
+ response = await client.get(url, headers=headers, params=params)
121
+ response.raise_for_status()
122
+ data = response.json()
123
+ return data.get("total_networth_usd", "0")
124
+ except Exception as exc: # pragma: no cover - log path only
125
+ logger.error("Error getting wallet net worth for %s: %s", wallet_address, exc)
126
+ return "0"
127
+
128
+
129
+ async def _build_assets_list(
130
+ agent: Agent, agent_data: AgentData, web3_client: Web3
131
+ ) -> list[Asset]:
132
+ """Build the assets list based on network conditions and agent configuration."""
133
+ assets: list[Asset] = []
134
+
135
+ if not agent_data or not agent_data.evm_wallet_address:
136
+ return assets
137
+
138
+ wallet_address = agent_data.evm_wallet_address
139
+ network_id: Optional[str] = agent.network_id
140
+
141
+ # ETH is always included
142
+ eth_balance = await _get_eth_balance(web3_client, wallet_address)
143
+ assets.append(Asset(symbol="ETH", balance=eth_balance))
144
+
145
+ if network_id and network_id.endswith("-mainnet"):
146
+ usdc_address = USDC_ADDRESSES.get(str(network_id))
147
+ if usdc_address:
148
+ usdc_balance = await _get_token_balance(
149
+ web3_client, wallet_address, usdc_address
150
+ )
151
+ assets.append(Asset(symbol="USDC", balance=usdc_balance))
152
+
153
+ if network_id == "base-mainnet":
154
+ nation_balance = await _get_token_balance(
155
+ web3_client, wallet_address, NATION_ADDRESS
156
+ )
157
+ assets.append(Asset(symbol="NATION", balance=nation_balance))
158
+
159
+ if agent.ticker and agent.token_address:
160
+ lower_addresses = [addr.lower() for addr in USDC_ADDRESSES.values()]
161
+ is_usdc = agent.token_address.lower() in lower_addresses
162
+ is_nation = agent.token_address.lower() == NATION_ADDRESS.lower()
163
+
164
+ if not is_usdc and not is_nation:
165
+ custom_balance = await _get_token_balance(
166
+ web3_client, wallet_address, agent.token_address
167
+ )
168
+ assets.append(Asset(symbol=agent.ticker, balance=custom_balance))
169
+
170
+ return assets
171
+
172
+
173
+ async def agent_asset(agent_id: str) -> AgentAssets:
174
+ """Fetch wallet net worth and token balances for an agent."""
175
+ agent = await Agent.get(agent_id)
176
+ if not agent:
177
+ raise IntentKitAPIError(404, "AgentNotFound", "Agent not found")
178
+
179
+ agent_data = await AgentData.get(agent_id)
180
+ if not agent_data or not agent_data.evm_wallet_address:
181
+ return AgentAssets(net_worth="0", tokens=[])
182
+
183
+ if not agent.network_id:
184
+ return AgentAssets(net_worth="0", tokens=[])
185
+
186
+ try:
187
+ web3_client = get_web3_client(str(agent.network_id), agent_store)
188
+ tokens = await _build_assets_list(agent, agent_data, web3_client)
189
+ net_worth = await _get_wallet_net_worth(agent_data.evm_wallet_address)
190
+ return AgentAssets(net_worth=net_worth, tokens=tokens)
191
+ except IntentKitAPIError:
192
+ raise
193
+ except Exception as exc:
194
+ logger.error("Error getting agent assets for %s: %s", agent_id, exc)
195
+ raise IntentKitAPIError(
196
+ 500, "AgentAssetError", "Failed to retrieve agent assets"
197
+ ) from exc
198
+
199
+
200
+ __all__ = [
201
+ "Asset",
202
+ "AgentAssets",
203
+ "USDC_ADDRESSES",
204
+ "NATION_ADDRESS",
205
+ "agent_asset",
206
+ "_build_assets_list",
207
+ "_get_wallet_net_worth",
208
+ ]
intentkit/models/redis.py CHANGED
@@ -74,6 +74,9 @@ def get_redis() -> Redis:
74
74
  return _redis_client
75
75
 
76
76
 
77
+ DEFAULT_HEARTBEAT_TTL = 16 * 60
78
+
79
+
77
80
  async def send_heartbeat(redis_client: Redis, name: str) -> None:
78
81
  """Set a heartbeat key in Redis that expires after 16 minutes.
79
82
 
@@ -83,7 +86,7 @@ async def send_heartbeat(redis_client: Redis, name: str) -> None:
83
86
  """
84
87
  try:
85
88
  key = f"intentkit:heartbeat:{name}"
86
- await redis_client.set(key, 1, ex=190) # 190 seconds = 3 minutes
89
+ await redis_client.set(key, 1, ex=DEFAULT_HEARTBEAT_TTL)
87
90
  except Exception as e:
88
91
  logger.error(f"Failed to send heartbeat for {name}: {e}")
89
92
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: intentkit
3
- Version: 0.7.5.dev25
3
+ Version: 0.7.5.dev27
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
@@ -1,4 +1,4 @@
1
- intentkit/__init__.py,sha256=3UT9gVXr5a-tj1qyFS0czSTwnLsYJFe6krz8INm3vCM,384
1
+ intentkit/__init__.py,sha256=ZlI7silpSeAQckf-Hy1KZV1XxlGwfAHEN388zXvsYHg,384
2
2
  intentkit/abstracts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  intentkit/abstracts/agent.py,sha256=108gb5W8Q1Sy4G55F2_ZFv2-_CnY76qrBtpIr0Oxxqk,1489
4
4
  intentkit/abstracts/api.py,sha256=ZUc24vaQvQVbbjznx7bV0lbbQxdQPfEV8ZxM2R6wZWo,166
@@ -15,6 +15,7 @@ intentkit/config/config.py,sha256=kw9F-uLsJd-knCKmYNb-hqR7x7HUXUkNg5FZCgOPH54,88
15
15
  intentkit/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  intentkit/core/agent.py,sha256=7WTUouAV3uLvsJxHq4JziCMO8YdYeVujtmc--9L5ZJc,31981
17
17
  intentkit/core/api.py,sha256=WfoaHNquujYJIpNPuTR1dSaaxog0S3X2W4lG9Ehmkm4,3284
18
+ intentkit/core/asset.py,sha256=mswjgAhSkAzdkz8VlFCWFMrE4Di5R3tZlkHtC0Rt4D0,7397
18
19
  intentkit/core/chat.py,sha256=YN20CnDazWLjiOZFOHgV6uHmA2DKkvPCsD5Q5sfNcZg,1685
19
20
  intentkit/core/client.py,sha256=J5K7f08-ucszBKAbn9K3QNOFKIC__7amTbKYii1jFkI,3056
20
21
  intentkit/core/credit.py,sha256=b4f4T6G6eeBTMe0L_r8awWtXgUnqiog4IUaymDPYym0,75587
@@ -35,7 +36,7 @@ intentkit/models/db_mig.py,sha256=vT6Tanm-BHC2T7dTztuB1UG494EFBAlHADKsNzR6xaQ,35
35
36
  intentkit/models/generator.py,sha256=lyZu9U9rZUGkqd_QT5SAhay9DY358JJY8EhDSpN8I1M,10298
36
37
  intentkit/models/llm.csv,sha256=R298CcmK-pcD8-2Q8xPgaymu6Gqc50yBFtBjRQI4aXM,3403
37
38
  intentkit/models/llm.py,sha256=ZnM6qpk9ouTqFysQzzg8TcAfxXASD-H9f1eHsmVjYv8,22186
38
- intentkit/models/redis.py,sha256=UoN8jqLREO1VO9_w6m-JhldpP19iEHj4TiGVCMutQW4,3702
39
+ intentkit/models/redis.py,sha256=Vqb9pjeMQ85Az5GvUbqCsQ5stBpFg3n85p94TB40xEw,3727
39
40
  intentkit/models/skill.py,sha256=n9d1VKWxACe852wMOUC5P7Ps1EUMzIDAYT62Vfs8pQU,20826
40
41
  intentkit/models/skills.csv,sha256=lvQg_1byPvJaHToQbEqRZarfYDuXpP4EXeVJVMc8aDs,19132
41
42
  intentkit/models/user.py,sha256=r2UWpuBJbS6bbfS-fz_rAtOTHL3zodRt1rccA7HhAQM,9902
@@ -270,8 +271,6 @@ intentkit/skills/moralis/fetch_solana_portfolio.py,sha256=rc6uqqt6_13VoveNf1mxnC
270
271
  intentkit/skills/moralis/fetch_wallet_portfolio.py,sha256=tujwVQklkkaDTnLj6Ce-hUybg_0gWr-GLTaocEmzNA4,11166
271
272
  intentkit/skills/moralis/moralis.png,sha256=fUm771g9SmL3SO2m0yeEAzrnT0C_scj_r9rowCvFiVo,11232
272
273
  intentkit/skills/moralis/schema.json,sha256=jpD66pyw1LtUXejAjWHNJy_FGXeHK-YRlRMtvFIA33g,4734
273
- intentkit/skills/moralis/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
274
- intentkit/skills/moralis/tests/test_wallet.py,sha256=MI4WwFNBoILDEXlK22glntiySgR9BWkpHUdIfKUu2-A,18610
275
274
  intentkit/skills/morpho/__init__.py,sha256=03BU7URDdBCu7PEl9JNvZ1JJ0GSy3Xk7tiQJzJuNc84,1482
276
275
  intentkit/skills/morpho/base.py,sha256=FYbgul_35OX-oHwTqA8Jb99V9azM1AVcsUqGScOvQAg,238
277
276
  intentkit/skills/morpho/morpho.svg,sha256=v3snRB2CkMbaw36YbdmNRuptu5I07O-6WUw__Htciv8,602
@@ -450,7 +449,7 @@ intentkit/utils/random.py,sha256=DymMxu9g0kuQLgJUqalvgksnIeLdS-v0aRk5nQU0mLI,452
450
449
  intentkit/utils/s3.py,sha256=A8Nsx5QJyLsxhj9g7oHNy2-m24tjQUhC9URm8Qb1jFw,10057
451
450
  intentkit/utils/slack_alert.py,sha256=s7UpRgyzLW7Pbmt8cKzTJgMA9bm4EP-1rQ5KXayHu6E,2264
452
451
  intentkit/utils/tx.py,sha256=2yLLGuhvfBEY5n_GJ8wmIWLCzn0FsYKv5kRNzw_sLUI,1454
453
- intentkit-0.7.5.dev25.dist-info/METADATA,sha256=-apoONdJcKpfNbfjm6CW-0EnxodTZpiGnjJL_WXqgrc,6360
454
- intentkit-0.7.5.dev25.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
455
- intentkit-0.7.5.dev25.dist-info/licenses/LICENSE,sha256=Bln6DhK-LtcO4aXy-PBcdZv2f24MlJFm_qn222biJtE,1071
456
- intentkit-0.7.5.dev25.dist-info/RECORD,,
452
+ intentkit-0.7.5.dev27.dist-info/METADATA,sha256=YHQ44tLldCDRz_-fIBT0fvgJbBzf1xirCpNtCNRLTN0,6360
453
+ intentkit-0.7.5.dev27.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
454
+ intentkit-0.7.5.dev27.dist-info/licenses/LICENSE,sha256=Bln6DhK-LtcO4aXy-PBcdZv2f24MlJFm_qn222biJtE,1071
455
+ intentkit-0.7.5.dev27.dist-info/RECORD,,
File without changes
@@ -1,511 +0,0 @@
1
- """Tests for the Moralis Wallet Portfolio skills."""
2
-
3
- import asyncio
4
- import json
5
- import unittest
6
- from unittest.mock import AsyncMock, MagicMock, patch
7
-
8
- from intentkit.skills.moralis import (
9
- FetchChainPortfolio,
10
- FetchSolanaPortfolio,
11
- FetchWalletPortfolio,
12
- get_skills,
13
- )
14
- from intentkit.skills.moralis.api import (
15
- fetch_moralis_data,
16
- fetch_wallet_balances,
17
- get_solana_portfolio,
18
- )
19
- from intentkit.skills.moralis.base import WalletBaseTool
20
-
21
-
22
- class DummyResponse:
23
- """Mock HTTP response for testing."""
24
-
25
- def __init__(self, status_code, json_data):
26
- self.status_code = status_code
27
- self._json_data = json_data
28
- self.text = json.dumps(json_data) if json_data else ""
29
-
30
- def json(self):
31
- return self._json_data
32
-
33
- async def raise_for_status(self):
34
- if self.status_code >= 400:
35
- raise Exception(f"HTTP Error: {self.status_code}")
36
-
37
-
38
- class TestWalletBaseClass(unittest.TestCase):
39
- """Test the base wallet portfolio tool class."""
40
-
41
- def setUp(self):
42
- self.loop = asyncio.new_event_loop()
43
- asyncio.set_event_loop(self.loop)
44
-
45
- self.mock_skill_store = MagicMock()
46
-
47
- def tearDown(self):
48
- self.loop.close()
49
-
50
- def test_base_class_init(self):
51
- """Test base class initialization."""
52
-
53
- # Create a concrete subclass for testing
54
- class TestTool(WalletBaseTool):
55
- async def _arun(self, *args, **kwargs):
56
- return "test"
57
-
58
- tool = TestTool(
59
- name="test_tool",
60
- description="Test tool",
61
- args_schema=MagicMock(),
62
- api_key="test_key",
63
- skill_store=self.mock_skill_store,
64
- agent_id="test_agent",
65
- )
66
-
67
- self.assertEqual(tool.api_key, "test_key")
68
- self.assertEqual(tool.agent_id, "test_agent")
69
- self.assertEqual(tool.skill_store, self.mock_skill_store)
70
- self.assertEqual(tool.category, "moralis")
71
-
72
- def test_get_chain_name(self):
73
- """Test chain name conversion."""
74
-
75
- class TestTool(WalletBaseTool):
76
- async def _arun(self, *args, **kwargs):
77
- return "test"
78
-
79
- tool = TestTool(
80
- name="test_tool",
81
- description="Test tool",
82
- args_schema=MagicMock(),
83
- api_key="test_key",
84
- skill_store=self.mock_skill_store,
85
- agent_id="test_agent",
86
- )
87
-
88
- # Test with known chain IDs
89
- self.assertEqual(tool._get_chain_name(1), "eth")
90
- self.assertEqual(tool._get_chain_name(56), "bsc")
91
- self.assertEqual(tool._get_chain_name(137), "polygon")
92
-
93
- # Test with unknown chain ID
94
- self.assertEqual(tool._get_chain_name(999999), "eth")
95
-
96
-
97
- class TestAPIFunctions(unittest.IsolatedAsyncioTestCase):
98
- """Test the API interaction functions."""
99
-
100
- async def test_fetch_moralis_data(self):
101
- """Test the base Moralis API function."""
102
- with patch("httpx.AsyncClient") as MockClient:
103
- client_instance = AsyncMock()
104
- client_instance.get.return_value = DummyResponse(
105
- 200, {"success": True, "data": "test_data"}
106
- )
107
- MockClient.return_value.__aenter__.return_value = client_instance
108
-
109
- result = await fetch_moralis_data(
110
- "test_api_key", "test_endpoint", "0xAddress", 1
111
- )
112
-
113
- self.assertEqual(result, {"success": True, "data": "test_data"})
114
-
115
- # Test error handling
116
- client_instance.get.return_value = DummyResponse(404, None)
117
- client_instance.get.return_value.raise_for_status = AsyncMock(
118
- side_effect=Exception("HTTP error 404")
119
- )
120
-
121
- result = await fetch_moralis_data(
122
- "test_api_key", "test_endpoint", "0xAddress", 1
123
- )
124
- self.assertIn("error", result)
125
-
126
- async def test_fetch_wallet_balances(self):
127
- """Test fetching wallet balances."""
128
- with patch("skills.moralis.api.fetch_moralis_data") as mock_fetch:
129
- mock_fetch.return_value = {
130
- "result": [
131
- {
132
- "token_address": "0x123",
133
- "symbol": "TEST",
134
- "balance": "1000000",
135
- "usd_value": 100,
136
- }
137
- ]
138
- }
139
-
140
- result = await fetch_wallet_balances("test_api_key", "0xAddress", 1)
141
-
142
- self.assertEqual(result["result"][0]["symbol"], "TEST")
143
- mock_fetch.assert_called_once_with(
144
- "test_api_key", "wallets/{address}/tokens", "0xAddress", 1, None
145
- )
146
-
147
- async def test_get_solana_portfolio(self):
148
- """Test getting Solana portfolio."""
149
- with patch("skills.moralis.api.fetch_solana_api") as mock_fetch:
150
- mock_fetch.return_value = {
151
- "nativeBalance": {"solana": 1.5, "lamports": 1500000000},
152
- "tokens": [
153
- {
154
- "symbol": "TEST",
155
- "name": "Test Token",
156
- "mint": "TokenMintAddress",
157
- "associatedTokenAddress": "AssocTokenAddress",
158
- "amount": 10,
159
- "decimals": 9,
160
- "amountRaw": "10000000000",
161
- }
162
- ],
163
- }
164
-
165
- result = await get_solana_portfolio("test_api_key", "SolAddress", "mainnet")
166
-
167
- mock_fetch.assert_called_once_with(
168
- "test_api_key", "/account/mainnet/SolAddress/portfolio"
169
- )
170
- self.assertEqual(result["nativeBalance"]["solana"], 1.5)
171
- self.assertEqual(len(result["tokens"]), 1)
172
- self.assertEqual(result["tokens"][0]["symbol"], "TEST")
173
-
174
-
175
- class TestFetchWalletPortfolio(unittest.IsolatedAsyncioTestCase):
176
- """Test the FetchWalletPortfolio skill."""
177
-
178
- async def test_wallet_portfolio_success(self):
179
- """Test successful wallet portfolio fetch."""
180
- mock_skill_store = MagicMock()
181
-
182
- with (
183
- patch(
184
- "skills.moralis.moralis_fetch_wallet_portfolio.fetch_wallet_balances"
185
- ) as mock_balances,
186
- patch(
187
- "skills.moralis.moralis_fetch_wallet_portfolio.fetch_net_worth"
188
- ) as mock_net_worth,
189
- ):
190
- # Mock successful responses
191
- mock_balances.return_value = {
192
- "result": [
193
- {
194
- "token_address": "0x123",
195
- "symbol": "TEST",
196
- "name": "Test Token",
197
- "balance": "1000000000000000000",
198
- "balance_formatted": "1.0",
199
- "usd_value": 100,
200
- }
201
- ]
202
- }
203
- mock_net_worth.return_value = {"result": {"total_networth_usd": 1000}}
204
-
205
- tool = FetchWalletPortfolio(
206
- name="fetch_wallet_portfolio",
207
- description="Test description",
208
- args_schema=MagicMock(),
209
- api_key="test_key",
210
- skill_store=mock_skill_store,
211
- agent_id="test_agent",
212
- )
213
-
214
- result = await tool._arun(address="0xAddress")
215
-
216
- self.assertEqual(result.address, "0xAddress")
217
- self.assertEqual(result.total_net_worth, 1000)
218
- self.assertEqual(len(result.tokens), 1)
219
- self.assertEqual(result.tokens[0].symbol, "TEST")
220
-
221
- async def test_wallet_portfolio_with_solana(self):
222
- """Test wallet portfolio with Solana support."""
223
- mock_skill_store = MagicMock()
224
-
225
- with (
226
- patch(
227
- "skills.moralis.moralis_fetch_wallet_portfolio.fetch_wallet_balances"
228
- ) as mock_evm_balances,
229
- patch(
230
- "skills.moralis.moralis_fetch_wallet_portfolio.fetch_net_worth"
231
- ) as mock_net_worth,
232
- patch(
233
- "skills.moralis.moralis_fetch_wallet_portfolio.get_solana_portfolio"
234
- ) as mock_sol_portfolio,
235
- patch(
236
- "skills.moralis.moralis_fetch_wallet_portfolio.get_token_price"
237
- ) as mock_token_price,
238
- ):
239
- # Mock EVM responses
240
- mock_evm_balances.return_value = {
241
- "result": [
242
- {
243
- "token_address": "0x123",
244
- "symbol": "ETH",
245
- "name": "Ethereum",
246
- "balance": "1000000000000000000",
247
- "balance_formatted": "1.0",
248
- "usd_value": 2000,
249
- }
250
- ]
251
- }
252
- mock_net_worth.return_value = {"result": {"total_networth_usd": 3000}}
253
-
254
- # Mock Solana responses
255
- mock_sol_portfolio.return_value = {
256
- "nativeBalance": {"solana": 2.0, "lamports": 2000000000},
257
- "tokens": [
258
- {
259
- "symbol": "SOL",
260
- "name": "Solana",
261
- "mint": "So11111111111111111111111111111111111111112",
262
- "associatedTokenAddress": "AssocTokenAddress",
263
- "amount": 2.0,
264
- "decimals": 9,
265
- "amountRaw": "2000000000",
266
- }
267
- ],
268
- }
269
-
270
- mock_token_price.return_value = {"usdPrice": 500}
271
-
272
- tool = FetchWalletPortfolio(
273
- name="fetch_wallet_portfolio",
274
- description="Test description",
275
- args_schema=MagicMock(),
276
- api_key="test_key",
277
- skill_store=mock_skill_store,
278
- agent_id="test_agent",
279
- )
280
-
281
- result = await tool._arun(address="0xAddress", include_solana=True)
282
-
283
- self.assertEqual(result.address, "0xAddress")
284
- self.assertEqual(
285
- result.total_net_worth, 3000
286
- ) # Using the net worth from mock
287
- self.assertIn("eth", result.chains)
288
- self.assertIn("solana", result.chains)
289
-
290
- # Check that we have both EVM and Solana tokens
291
- token_symbols = [token.symbol for token in result.tokens]
292
- self.assertIn("ETH", token_symbols)
293
- self.assertIn("SOL", token_symbols)
294
-
295
-
296
- class TestFetchSolanaPortfolio(unittest.IsolatedAsyncioTestCase):
297
- """Test the FetchSolanaPortfolio skill."""
298
-
299
- async def test_solana_portfolio_success(self):
300
- """Test successful Solana portfolio fetch."""
301
- mock_skill_store = MagicMock()
302
-
303
- with (
304
- patch(
305
- "skills.moralis.moralis_fetch_solana_portfolio.get_solana_portfolio"
306
- ) as mock_portfolio,
307
- patch(
308
- "skills.moralis.moralis_fetch_solana_portfolio.get_solana_nfts"
309
- ) as mock_nfts,
310
- patch(
311
- "skills.moralis.moralis_fetch_solana_portfolio.get_token_price"
312
- ) as mock_token_price,
313
- ):
314
- # Mock successful responses
315
- mock_portfolio.return_value = {
316
- "nativeBalance": {"solana": 1.5, "lamports": 1500000000},
317
- "tokens": [
318
- {
319
- "symbol": "TEST",
320
- "name": "Test Token",
321
- "mint": "TokenMintAddress",
322
- "associatedTokenAddress": "AssocTokenAddress",
323
- "amount": 10,
324
- "decimals": 9,
325
- "amountRaw": "10000000000",
326
- }
327
- ],
328
- }
329
-
330
- mock_nfts.return_value = [
331
- {
332
- "mint": "NFTMintAddress",
333
- "name": "Test NFT",
334
- "symbol": "TNFT",
335
- "associatedTokenAddress": "AssocTokenAddress",
336
- "metadata": {"name": "Test NFT", "image": "image.png"},
337
- }
338
- ]
339
-
340
- mock_token_price.return_value = {"usdPrice": 25}
341
-
342
- tool = FetchSolanaPortfolio(
343
- name="fetch_solana_portfolio",
344
- description="Test description",
345
- args_schema=MagicMock(),
346
- api_key="test_key",
347
- skill_store=mock_skill_store,
348
- agent_id="test_agent",
349
- )
350
-
351
- result = await tool._arun(address="SolanaAddress", include_nfts=True)
352
-
353
- self.assertEqual(result.address, "SolanaAddress")
354
- self.assertEqual(result.sol_balance, 1.5)
355
- self.assertEqual(len(result.tokens), 1)
356
- self.assertEqual(result.tokens[0].token_info.symbol, "TEST")
357
- self.assertEqual(len(result.nfts), 1)
358
- self.assertEqual(result.nfts[0].name, "Test NFT")
359
- self.assertEqual(result.sol_price_usd, 25)
360
- self.assertEqual(result.sol_value_usd, 37.5) # 1.5 SOL * $25
361
-
362
-
363
- class TestFetchChainPortfolio(unittest.IsolatedAsyncioTestCase):
364
- """Test the FetchChainPortfolio skill."""
365
-
366
- async def test_chain_portfolio_success(self):
367
- """Test successful chain portfolio fetch."""
368
- mock_skill_store = MagicMock()
369
-
370
- with patch(
371
- "skills.moralis.moralis_fetch_chain_portfolio.fetch_wallet_balances"
372
- ) as mock_balances:
373
- # Mock successful responses
374
- mock_balances.return_value = {
375
- "result": [
376
- {
377
- "token_address": "0x123",
378
- "symbol": "ETH",
379
- "name": "Ethereum",
380
- "logo": "logo.png",
381
- "decimals": 18,
382
- "balance": "1000000000000000000",
383
- "balance_formatted": "1.0",
384
- "usd_value": 2000,
385
- "native_token": True,
386
- },
387
- {
388
- "token_address": "0x456",
389
- "symbol": "TOKEN",
390
- "name": "Test Token",
391
- "logo": "logo2.png",
392
- "decimals": 18,
393
- "balance": "2000000000000000000",
394
- "balance_formatted": "2.0",
395
- "usd_value": 200,
396
- "native_token": False,
397
- },
398
- ]
399
- }
400
-
401
- tool = FetchChainPortfolio(
402
- name="fetch_chain_portfolio",
403
- description="Test description",
404
- args_schema=MagicMock(),
405
- api_key="test_key",
406
- skill_store=mock_skill_store,
407
- agent_id="test_agent",
408
- )
409
-
410
- result = await tool._arun(address="0xAddress", chain_id=1)
411
-
412
- self.assertEqual(result.address, "0xAddress")
413
- self.assertEqual(result.chain_id, 1)
414
- self.assertEqual(result.chain_name, "eth")
415
- self.assertEqual(result.total_usd_value, 2200) # 2000 + 200
416
- self.assertEqual(len(result.tokens), 1) # Regular tokens, not native
417
- self.assertIsNotNone(result.native_token)
418
- self.assertEqual(result.native_token.symbol, "ETH")
419
- self.assertEqual(result.tokens[0].symbol, "TOKEN")
420
-
421
-
422
- class TestSkillInitialization(unittest.TestCase):
423
- """Test skill initialization and configuration."""
424
-
425
- def setUp(self):
426
- self.mock_skill_store = MagicMock()
427
-
428
- def test_get_skills(self):
429
- """Test getting multiple skills from config."""
430
- config = {
431
- "api_key": "test_api_key",
432
- "states": {
433
- "fetch_wallet_portfolio": "public",
434
- "fetch_chain_portfolio": "public",
435
- "fetch_nft_portfolio": "private",
436
- "fetch_transaction_history": "private",
437
- "fetch_solana_portfolio": "public",
438
- },
439
- "supported_chains": {"evm": True, "solana": True},
440
- }
441
-
442
- # Test with mock implementation
443
- with patch("skills.moralis.base.WalletBaseTool") as mock_tool:
444
- mock_tool.return_value = MagicMock()
445
-
446
- # This is just a test structure - actual implementation would create the skills
447
- skills = get_skills(
448
- config,
449
- is_private=False, # Only get public skills
450
- skill_store=self.mock_skill_store,
451
- agent_id="test_agent",
452
- )
453
-
454
- # In a real implementation, we'd test that the correct skills were returned
455
- # For now, we just verify the function exists
456
- self.assertIsNotNone(skills)
457
-
458
-
459
- class TestIntegration(unittest.TestCase):
460
- """Integration tests for wallet skills."""
461
-
462
- def test_wallet_skill_configuration(self):
463
- """Test wallet skill configuration in agent config."""
464
- # Example agent configuration
465
- agent_config = {
466
- "id": "crypto-agent",
467
- "skills": {
468
- "moralis": {
469
- "api_key": "test_api_key",
470
- "states": {
471
- "fetch_wallet_portfolio": "public",
472
- "fetch_chain_portfolio": "public",
473
- "fetch_nft_portfolio": "private",
474
- "fetch_transaction_history": "private",
475
- "fetch_solana_portfolio": "public",
476
- },
477
- "supported_chains": {"evm": True, "solana": True},
478
- }
479
- },
480
- }
481
-
482
- # Verify the configuration structure is valid
483
- moralis_config = agent_config["skills"]["moralis"]
484
- self.assertIn("api_key", moralis_config)
485
- self.assertIn("states", moralis_config)
486
- self.assertIn("supported_chains", moralis_config)
487
-
488
- # Check that all required skills are configured
489
- states = moralis_config["states"]
490
- required_skills = [
491
- "fetch_wallet_portfolio",
492
- "fetch_chain_portfolio",
493
- "fetch_nft_portfolio",
494
- "fetch_transaction_history",
495
- "fetch_solana_portfolio",
496
- ]
497
-
498
- for skill in required_skills:
499
- self.assertIn(skill, states)
500
- self.assertIn(states[skill], ["public", "private", "disabled"])
501
-
502
- # Check chain configuration
503
- chains = moralis_config["supported_chains"]
504
- self.assertIn("evm", chains)
505
- self.assertIn("solana", chains)
506
- self.assertTrue(isinstance(chains["evm"], bool))
507
- self.assertTrue(isinstance(chains["solana"], bool))
508
-
509
-
510
- if __name__ == "__main__":
511
- unittest.main()