Skip to content

Commit 5b56fd1

Browse files
authored
Merge pull request #1 from bnb-chain/feat/sdk-docs-and-tests
feat: improve SDK docs, add wallet tests, and fix registrations flow
1 parent b4b9f57 commit 5b56fd1

8 files changed

Lines changed: 1242 additions & 291 deletions

File tree

README.md

Lines changed: 58 additions & 282 deletions
Large diffs are not rendered by default.

bnbagent/README.md

Lines changed: 578 additions & 0 deletions
Large diffs are not rendered by default.

bnbagent/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
Network configuration constants.
33
"""
44

5+
# 8004scan API base URL
6+
SCAN_API_URL = "https://www.8004scan.io/api/v1"
7+
58
# BSC Testnet configuration
69
TESTNET_CONFIG = {
710
"name": "bsc-testnet",

bnbagent/erc8004_agent.py

Lines changed: 123 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from .wallets import WalletProvider
1616
from .contract import ContractInterface
1717
from .models import AgentEndpoint
18-
from .constants import TESTNET_CONFIG
18+
from .constants import TESTNET_CONFIG, SCAN_API_URL
1919
from .paymaster import Paymaster
2020

2121

@@ -336,8 +336,52 @@ def register_agent(
336336
agent_uri=agent_uri, metadata=metadata
337337
)
338338

339-
# Add agentURI to result
340-
result["agentURI"] = agent_uri
339+
# Get the assigned agentId
340+
agent_id = result.get("agentId")
341+
342+
# Regenerate agent URI with agentId and agentRegistry in registrations field
343+
final_agent_uri = agent_uri
344+
if agent_id is not None:
345+
try:
346+
# Rebuild endpoints from parsed data
347+
endpoints = []
348+
for svc in agent_data.get("services", []):
349+
endpoints.append(
350+
AgentEndpoint(
351+
name=svc.get("name", ""),
352+
endpoint=svc.get("endpoint", ""),
353+
version=svc.get("version"),
354+
)
355+
)
356+
357+
if endpoints:
358+
# Regenerate URI with agentId included
359+
final_agent_uri = self.generate_agent_uri(
360+
name=agent_data.get("name", ""),
361+
description=agent_data.get("description", ""),
362+
image=agent_data.get("image"),
363+
endpoints=endpoints,
364+
agent_id=agent_id,
365+
supported_trust=agent_data.get("supportedTrust")
366+
or agent_data.get("supportedTrusts"),
367+
)
368+
369+
# Update on-chain URI with registrations field populated
370+
self._logger.debug(
371+
f"Updating agent URI with registrations for agentId={agent_id}"
372+
)
373+
self.contract.set_agent_uri(agent_id, final_agent_uri)
374+
self._logger.info(
375+
f"Updated agent URI with registrations (agentId={agent_id})"
376+
)
377+
except Exception as e:
378+
self._logger.warning(
379+
f"Failed to update agent URI with registrations: {str(e)}. "
380+
"The agent is registered but registrations field may be empty."
381+
)
382+
383+
# Add final agentURI to result
384+
result["agentURI"] = final_agent_uri
341385

342386
# Save registered agent to state file (add to list)
343387
try:
@@ -352,7 +396,7 @@ def register_agent(
352396
# Update existing agent info
353397
registered_agents[i] = {
354398
"name": agent_name,
355-
"agent_uri": agent_uri,
399+
"agent_uri": final_agent_uri,
356400
"agent_id": result.get("agentId"),
357401
"transaction_hash": result.get("transactionHash"),
358402
}
@@ -364,7 +408,7 @@ def register_agent(
364408
registered_agents.append(
365409
{
366410
"name": agent_name,
367-
"agent_uri": agent_uri,
411+
"agent_uri": final_agent_uri,
368412
"agent_id": result.get("agentId"),
369413
"transaction_hash": result.get("transactionHash"),
370414
}
@@ -423,6 +467,80 @@ def get_agent_info(self, agent_id: int) -> Dict[str, Any]:
423467
self._logger.error(f"Failed to get agent info: {str(e)}")
424468
raise
425469

470+
def get_all_agents(
471+
self,
472+
limit: int = 10,
473+
offset: int = 0,
474+
) -> Dict[str, Any]:
475+
"""
476+
List all registered agents.
477+
478+
This method queries the indexer API to discover registered agents.
479+
It does not require on-chain calls.
480+
481+
Args:
482+
limit: Maximum number of agents to return (default: 10, max: 100)
483+
offset: Number of agents to skip for pagination (default: 0)
484+
485+
Returns:
486+
dict: Response containing:
487+
- items: List of agent objects with fields like:
488+
- token_id: Agent ID
489+
- name: Agent name
490+
- description: Agent description
491+
- owner_address: Owner wallet address
492+
- services: Dict of service endpoints
493+
- total_score: Reputation score
494+
- total: Total number of agents matching query
495+
- limit: Limit used in query
496+
- offset: Offset used in query
497+
498+
Raises:
499+
ConnectionError: If API request fails
500+
501+
Example:
502+
>>> # List first 10 agents
503+
>>> agents = sdk.get_all_agents(limit=10)
504+
>>> for agent in agents['items']:
505+
... print(f"Agent #{agent['token_id']}: {agent['name']}")
506+
507+
>>> # Paginate through results
508+
>>> page1 = sdk.get_all_agents(limit=10, offset=0)
509+
>>> page2 = sdk.get_all_agents(limit=10, offset=10)
510+
"""
511+
chain_id = self._network_config.get("chain_id")
512+
513+
self._logger.debug(
514+
f"Fetching agents: chain_id={chain_id}, limit={limit}, offset={offset}"
515+
)
516+
517+
# Build query parameters
518+
params = {
519+
"chain_id": chain_id,
520+
"limit": min(limit, 100), # Cap at 100
521+
"offset": offset,
522+
}
523+
524+
try:
525+
response = requests.get(
526+
f"{SCAN_API_URL}/agents",
527+
params=params,
528+
timeout=30,
529+
)
530+
response.raise_for_status()
531+
532+
data = response.json()
533+
self._logger.debug(
534+
f"Retrieved {len(data.get('items', []))} agents "
535+
f"(total: {data.get('total', 0)})"
536+
)
537+
538+
return data
539+
540+
except requests.exceptions.RequestException as e:
541+
self._logger.error(f"Failed to fetch agents from 8004scan: {str(e)}")
542+
raise ConnectionError(f"8004scan API request failed: {str(e)}") from e
543+
426544
def get_metadata(self, agent_id: int, key: str) -> str:
427545
"""
428546
Get metadata value for an agent.

bnbagent/wallets/evm_wallet_provider.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,11 +254,11 @@ def sign_message(self, message: str) -> Dict[str, Any]:
254254
signable_message = encode_defunct(text=message)
255255
signed_message = self._account.sign_message(signable_message)
256256

257-
self._logger.debug(f"Message signed: hash={signed_message.messageHash.hex()}")
257+
self._logger.debug(f"Message signed: hash={signed_message.message_hash.hex()}")
258258

259259
# Return dict format for consistent interface across wallet providers
260260
return {
261-
"messageHash": signed_message.messageHash,
261+
"messageHash": signed_message.message_hash,
262262
"r": signed_message.r,
263263
"s": signed_message.s,
264264
"v": signed_message.v,

examples/testnet_usage.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def main():
4848
print(f"Contract: {sdk.contract_address}\n")
4949

5050
# Define agent configuration
51-
agent_name = "My Testnet Agent 03"
51+
agent_name = "My Testnet Agent 06"
5252
agent_uri = sdk.generate_agent_uri(
5353
name=agent_name,
5454
description="A test agent running on BSC Testnet",

tests/test_sdk.py

Lines changed: 183 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
"""
44

55
import pytest
6-
from unittest.mock import Mock, patch
6+
from unittest.mock import Mock, patch, MagicMock
77
from bnbagent import ERC8004Agent, AgentEndpoint
8+
import requests
89

910

1011
class TestERC8004Agent:
@@ -300,3 +301,184 @@ def test_generate_agent_uri_requires_endpoints(self, sdk):
300301

301302
with pytest.raises(ValueError, match="endpoints is required"):
302303
sdk.generate_agent_uri(name="Test", description="Test", endpoints=[])
304+
305+
def test_get_all_agents(self, sdk):
306+
"""Test get_all_agents API call"""
307+
with patch("bnbagent.erc8004_agent.requests.get") as mock_get:
308+
mock_response = Mock()
309+
mock_response.json.return_value = {
310+
"items": [
311+
{"token_id": 1, "name": "Agent 1"},
312+
{"token_id": 2, "name": "Agent 2"},
313+
],
314+
"total": 2,
315+
"limit": 10,
316+
"offset": 0,
317+
}
318+
mock_response.raise_for_status = Mock()
319+
mock_get.return_value = mock_response
320+
321+
result = sdk.get_all_agents(limit=10, offset=0)
322+
323+
assert "items" in result
324+
assert len(result["items"]) == 2
325+
assert result["total"] == 2
326+
327+
def test_get_all_agents_with_pagination(self, sdk):
328+
"""Test get_all_agents with pagination parameters"""
329+
with patch("bnbagent.erc8004_agent.requests.get") as mock_get:
330+
mock_response = Mock()
331+
mock_response.json.return_value = {
332+
"items": [{"token_id": 11, "name": "Agent 11"}],
333+
"total": 15,
334+
"limit": 5,
335+
"offset": 10,
336+
}
337+
mock_response.raise_for_status = Mock()
338+
mock_get.return_value = mock_response
339+
340+
result = sdk.get_all_agents(limit=5, offset=10)
341+
342+
assert result["offset"] == 10
343+
assert result["limit"] == 5
344+
mock_get.assert_called_once()
345+
call_args = mock_get.call_args
346+
assert call_args[1]["params"]["limit"] == 5
347+
assert call_args[1]["params"]["offset"] == 10
348+
349+
def test_get_all_agents_connection_error(self, sdk):
350+
"""Test get_all_agents handles connection errors"""
351+
with patch("bnbagent.erc8004_agent.requests.get") as mock_get:
352+
mock_get.side_effect = requests.exceptions.ConnectionError("Network error")
353+
354+
with pytest.raises(ConnectionError, match="8004scan API request failed"):
355+
sdk.get_all_agents()
356+
357+
def test_get_all_agents_uses_network_chain_id(self, sdk):
358+
"""Test get_all_agents uses chain_id from network config"""
359+
with patch("bnbagent.erc8004_agent.requests.get") as mock_get:
360+
mock_response = Mock()
361+
mock_response.json.return_value = {"items": [], "total": 0}
362+
mock_response.raise_for_status = Mock()
363+
mock_get.return_value = mock_response
364+
365+
sdk.get_all_agents()
366+
367+
call_args = mock_get.call_args
368+
# Should use chain_id from network config (97 for bsc-testnet)
369+
assert call_args[1]["params"]["chain_id"] == 97
370+
371+
def test_get_local_agent_info_not_found(self, sdk):
372+
"""Test get_local_agent_info returns None for non-existent agent"""
373+
with patch.object(sdk.state_manager, "get", return_value=[]):
374+
result = sdk.get_local_agent_info("NonExistent Agent")
375+
assert result is None
376+
377+
def test_get_local_agent_info_found(self, sdk):
378+
"""Test get_local_agent_info returns agent info when found"""
379+
registered_agents = [
380+
{
381+
"name": "Test Agent",
382+
"agent_uri": "data:application/json;base64,xxx",
383+
"agent_id": 1,
384+
"transaction_hash": "0x123",
385+
}
386+
]
387+
with patch.object(sdk.state_manager, "get", return_value=registered_agents):
388+
result = sdk.get_local_agent_info("Test Agent")
389+
assert result is not None
390+
assert result["name"] == "Test Agent"
391+
assert result["agent_id"] == 1
392+
393+
def test_get_local_agent_info_empty_name(self, sdk):
394+
"""Test get_local_agent_info with empty name"""
395+
result = sdk.get_local_agent_info("")
396+
assert result is None
397+
398+
result = sdk.get_local_agent_info(None)
399+
assert result is None
400+
401+
def test_wallet_address_property(self, sdk, mock_wallet_provider):
402+
"""Test wallet_address property"""
403+
assert sdk.wallet_address == mock_wallet_provider.address
404+
405+
def test_contract_address_property(self, sdk):
406+
"""Test contract_address property"""
407+
assert sdk.contract_address == self.DEFAULT_CONTRACT_ADDRESS
408+
409+
def test_network_property(self, sdk):
410+
"""Test network property returns config"""
411+
network = sdk.network
412+
assert "name" in network
413+
assert "chain_id" in network
414+
assert network["name"] == "bsc-testnet"
415+
416+
def test_generate_agent_uri_with_supported_trust(self, sdk):
417+
"""Test generate_agent_uri with supportedTrust field"""
418+
agent_uri = sdk.generate_agent_uri(
419+
name="Trust Agent",
420+
description="Agent with trust mechanisms",
421+
endpoints=[
422+
AgentEndpoint(
423+
name="A2A",
424+
endpoint="https://agent.example/.well-known/agent-card.json",
425+
)
426+
],
427+
supported_trust=["reputation", "crypto-economic"],
428+
)
429+
430+
assert isinstance(agent_uri, str)
431+
agent_data = sdk.parse_agent_uri(agent_uri)
432+
assert "supportedTrust" in agent_data
433+
assert "reputation" in agent_data["supportedTrust"]
434+
435+
def test_generate_agent_uri_with_image(self, sdk):
436+
"""Test generate_agent_uri with image field"""
437+
agent_uri = sdk.generate_agent_uri(
438+
name="Image Agent",
439+
description="Agent with image",
440+
endpoints=[
441+
AgentEndpoint(
442+
name="A2A",
443+
endpoint="https://agent.example/.well-known/agent-card.json",
444+
)
445+
],
446+
image="https://example.com/agent-image.png",
447+
)
448+
449+
agent_data = sdk.parse_agent_uri(agent_uri)
450+
assert agent_data["image"] == "https://example.com/agent-image.png"
451+
452+
453+
class TestERC8004AgentInitialization:
454+
"""Test cases for SDK initialization edge cases"""
455+
456+
def test_invalid_network_raises_error(self):
457+
"""Test that invalid network raises ValueError"""
458+
mock_wallet = Mock()
459+
mock_wallet.address = "0x" + "0" * 40
460+
461+
with pytest.raises(ValueError, match="Unknown network"):
462+
with patch("bnbagent.erc8004_agent.Web3") as mock_web3_class:
463+
mock_web3 = Mock()
464+
mock_web3.is_connected.return_value = True
465+
mock_web3_class.return_value = mock_web3
466+
ERC8004Agent(wallet_provider=mock_wallet, network="invalid-network")
467+
468+
def test_missing_wallet_provider_raises_error(self):
469+
"""Test that missing wallet_provider raises ValueError"""
470+
with pytest.raises(ValueError, match="wallet_provider is required"):
471+
ERC8004Agent(wallet_provider=None, network="bsc-testnet")
472+
473+
def test_rpc_connection_failure(self):
474+
"""Test that RPC connection failure raises ConnectionError"""
475+
mock_wallet = Mock()
476+
mock_wallet.address = "0x" + "0" * 40
477+
478+
with patch("bnbagent.erc8004_agent.Web3") as mock_web3_class:
479+
mock_web3 = Mock()
480+
mock_web3.is_connected.return_value = False
481+
mock_web3_class.return_value = mock_web3
482+
483+
with pytest.raises(ConnectionError, match="Failed to connect to RPC"):
484+
ERC8004Agent(wallet_provider=mock_wallet, network="bsc-testnet")

0 commit comments

Comments
 (0)