intentkit 0.7.5.dev26__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 +1 -1
- intentkit/core/asset.py +208 -0
- {intentkit-0.7.5.dev26.dist-info → intentkit-0.7.5.dev27.dist-info}/METADATA +1 -1
- {intentkit-0.7.5.dev26.dist-info → intentkit-0.7.5.dev27.dist-info}/RECORD +6 -7
- intentkit/skills/moralis/tests/__init__.py +0 -0
- intentkit/skills/moralis/tests/test_wallet.py +0 -511
- {intentkit-0.7.5.dev26.dist-info → intentkit-0.7.5.dev27.dist-info}/WHEEL +0 -0
- {intentkit-0.7.5.dev26.dist-info → intentkit-0.7.5.dev27.dist-info}/licenses/LICENSE +0 -0
intentkit/__init__.py
CHANGED
intentkit/core/asset.py
ADDED
|
@@ -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
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: intentkit
|
|
3
|
-
Version: 0.7.5.
|
|
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=
|
|
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
|
|
@@ -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.
|
|
454
|
-
intentkit-0.7.5.
|
|
455
|
-
intentkit-0.7.5.
|
|
456
|
-
intentkit-0.7.5.
|
|
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()
|
|
File without changes
|
|
File without changes
|