diff --git a/python/examples/06-send-tokens/main.py b/python/examples/06-send-tokens/main.py index 25e235ee..8988e515 100644 --- a/python/examples/06-send-tokens/main.py +++ b/python/examples/06-send-tokens/main.py @@ -36,7 +36,7 @@ async def request_funds(ctx: Context): @alice.on_message(model=TransactionInfo) async def confirm_transaction(ctx: Context, sender: str, msg: TransactionInfo): ctx.logger.info(f"Received transaction info from {sender}: {msg}") - tx_resp = await wait_for_tx_to_complete(msg.tx_hash) + tx_resp = await wait_for_tx_to_complete(msg.tx_hash, ctx.ledger) coin_received = tx_resp.events["coin_received"] if ( diff --git a/python/examples/08-local-network-interaction/agent2.py b/python/examples/08-local-network-interaction/agent2.py index 149bb27b..6b6b7a84 100644 --- a/python/examples/08-local-network-interaction/agent2.py +++ b/python/examples/08-local-network-interaction/agent2.py @@ -6,7 +6,9 @@ class Message(Model): message: str -RECIPIENT_ADDRESS = "agent1q2kxet3vh0scsf0sm7y2erzz33cve6tv5uk63x64upw5g68kr0chkv7hw50" +RECIPIENT_ADDRESS = ( + "test-agent://agent1q2kxet3vh0scsf0sm7y2erzz33cve6tv5uk63x64upw5g68kr0chkv7hw50" +) alice = Agent( name="alice", diff --git a/python/examples/09-booking-protocol-demo/user.py b/python/examples/09-booking-protocol-demo/user.py index fc09668a..192ec2f3 100644 --- a/python/examples/09-booking-protocol-demo/user.py +++ b/python/examples/09-booking-protocol-demo/user.py @@ -8,7 +8,9 @@ from uagents.setup import fund_agent_if_low -RESTAURANT_ADDRESS = "agent1qfpqn9jhvp9cg33f27q6jvmuv52dgyg9rfuu37rmxrletlqe7lewwjed5gy" +RESTAURANT_ADDRESS = ( + "test-agent://agent1qfpqn9jhvp9cg33f27q6jvmuv52dgyg9rfuu37rmxrletlqe7lewwjed5gy" +) user = Agent( name="user", diff --git a/python/examples/10-cleaning-demo/user.py b/python/examples/10-cleaning-demo/user.py index c0bc4cea..5c09768e 100644 --- a/python/examples/10-cleaning-demo/user.py +++ b/python/examples/10-cleaning-demo/user.py @@ -12,7 +12,9 @@ from uagents.setup import fund_agent_if_low -CLEANER_ADDRESS = "agent1qdfdx6952trs028fxyug7elgcktam9f896ays6u9art4uaf75hwy2j9m87w" +CLEANER_ADDRESS = ( + "test-agent://agent1qdfdx6952trs028fxyug7elgcktam9f896ays6u9art4uaf75hwy2j9m87w" +) user = Agent( name="user", diff --git a/python/examples/13-agent-name-service/agent1.py b/python/examples/13-agent-name-service/agent1.py index b70295fb..b023f9c5 100644 --- a/python/examples/13-agent-name-service/agent1.py +++ b/python/examples/13-agent-name-service/agent1.py @@ -1,6 +1,6 @@ from cosmpy.aerial.wallet import LocalWallet -from uagents.network import get_ledger, get_name_service_contract +from uagents.network import get_name_service_contract from uagents.setup import fund_agent_if_low from uagents import Agent, Context, Model @@ -19,11 +19,12 @@ class Message(Model): endpoint=["http://localhost:8001/submit"], ) -ledger = get_ledger() + my_wallet = LocalWallet.from_unsafe_seed("registration test wallet") -name_service_contract = get_name_service_contract() +name_service_contract = get_name_service_contract(test=True) DOMAIN = "agent" + for wallet in [my_wallet, bob.wallet]: fund_agent_if_low(wallet.address()) @@ -31,7 +32,7 @@ class Message(Model): @bob.on_event("startup") async def register_agent_name(ctx: Context): await name_service_contract.register( - ledger, my_wallet, ctx.address, ctx.name, DOMAIN + bob.ledger, my_wallet, ctx.address, ctx.name, DOMAIN ) diff --git a/python/examples/13-agent-name-service/agent2.py b/python/examples/13-agent-name-service/agent2.py index 10709f46..c10fb882 100644 --- a/python/examples/13-agent-name-service/agent2.py +++ b/python/examples/13-agent-name-service/agent2.py @@ -13,7 +13,6 @@ class Message(Model): endpoint=["http://localhost:8000/submit"], ) - fund_agent_if_low(alice.wallet.address()) diff --git a/python/src/uagents/agent.py b/python/src/uagents/agent.py index ae4f7ed6..972b6795 100644 --- a/python/src/uagents/agent.py +++ b/python/src/uagents/agent.py @@ -9,6 +9,7 @@ from cosmpy.aerial.wallet import LocalWallet, PrivateKey from cosmpy.crypto.address import Address +from cosmpy.aerial.client import LedgerClient from uagents.asgi import ASGIServer from uagents.context import ( @@ -34,6 +35,8 @@ REGISTRATION_UPDATE_INTERVAL_SECONDS, LEDGER_PREFIX, REGISTRATION_RETRY_INTERVAL_SECONDS, + TESTNET_PREFIX, + MAINNET_PREFIX, parse_endpoint_config, parse_agentverse_config, get_logger, @@ -125,10 +128,12 @@ class Agent(Sink): protocols (Dict[str, Protocol]): Dictionary mapping all supported protocol digests to their corresponding protocols. _ctx (Context): The context for agent interactions. + _test (bool): True if the agent will register and transact on the testnet. Properties: name (str): The name of the agent. address (str): The address of the agent used for communication. + identifier (str): The Agent Identifier, including network prefix and address. wallet (LocalWallet): The agent's wallet for transacting on the ledger. storage (KeyValueStore): The key-value store for storage operations. mailbox (Dict[str, str]): The mailbox configuration for the agent (deprecated and replaced @@ -151,6 +156,7 @@ def __init__( resolve: Optional[Resolver] = None, max_resolver_endpoints: Optional[int] = None, version: Optional[str] = None, + test: Optional[bool] = True, ): """ Initialize an Agent instance. @@ -210,8 +216,8 @@ def __init__( else: self._mailbox_client = None - self._ledger = get_ledger() - self._almanac_contract = get_almanac_contract() + self._ledger = get_ledger(test) + self._almanac_contract = get_almanac_contract(test) self._storage = KeyValueStore(self.address[0:16]) self._interval_handlers: List[Tuple[IntervalCallback, float]] = [] self._interval_messages: Set[str] = set() @@ -224,6 +230,7 @@ def __init__( self._message_queue = asyncio.Queue() self._on_startup = [] self._on_shutdown = [] + self._test = test self._version = version or "0.1.0" # initialize the internal agent protocol @@ -234,6 +241,7 @@ def __init__( self._ctx = Context( self._identity.address, + self.identifier, self._name, self._storage, self._resolver, @@ -308,6 +316,17 @@ def address(self) -> str: """ return self._identity.address + @property + def identifier(self) -> str: + """ + Get the Agent Identifier, including network prefix and address. + + Returns: + str: The agent's identifier. + """ + prefix = TESTNET_PREFIX if self._test else MAINNET_PREFIX + return prefix + self._identity.address + @property def wallet(self) -> LocalWallet: """ @@ -318,6 +337,16 @@ def wallet(self) -> LocalWallet: """ return self._wallet + @property + def ledger(self) -> LedgerClient: + """ + Get the ledger of the agent. + + Returns: + LedgerClient: The agent's ledger + """ + return self._ledger + @property def storage(self) -> KeyValueStore: """ @@ -358,6 +387,17 @@ def mailbox_client(self) -> MailboxClient: """ return self._mailbox_client + @property + def balance(self) -> int: + """ + Get the balance of the agent. + + Returns: + int: Bank balance. + """ + + return self.ledger.query_bank_balance(Address(self.wallet.address())) + @mailbox.setter def mailbox(self, config: Union[str, Dict[str, str]]): """ @@ -472,11 +512,7 @@ async def register(self): or list(self.protocols.keys()) != self._almanac_contract.get_protocols(self.address) ): - agent_balance = self._ledger.query_bank_balance( - Address(self.wallet.address()) - ) - - if agent_balance < REGISTRATION_FEE: + if self.balance < REGISTRATION_FEE: self._logger.warning( f"I do not have enough funds to register on Almanac contract\ \nFund using wallet address: {self.wallet.address()}" @@ -485,7 +521,7 @@ async def register(self): self._logger.info("Registering on almanac contract...") signature = self.sign_registration() await self._almanac_contract.register( - self._ledger, + self.ledger, self.wallet, self.address, list(self.protocols.keys()), @@ -806,6 +842,7 @@ async def _process_message_queue(self): context = Context( self._identity.address, + self.identifier, self._name, self._storage, self._resolver, diff --git a/python/src/uagents/config.py b/python/src/uagents/config.py index f22fbfdc..042f730a 100644 --- a/python/src/uagents/config.py +++ b/python/src/uagents/config.py @@ -1,6 +1,7 @@ import logging import sys -from enum import Enum + + from typing import Any, Dict, List, Optional, Union from uvicorn.logging import DefaultFormatter @@ -8,16 +9,22 @@ logging.basicConfig(level=logging.INFO) -class AgentNetwork(Enum): - FETCHAI_TESTNET = 1 - FETCHAI_MAINNET = 2 - - AGENT_PREFIX = "agent" LEDGER_PREFIX = "fetch" USER_PREFIX = "user" -CONTRACT_ALMANAC = "fetch1tjagw8g8nn4cwuw00cf0m5tl4l6wfw9c0ue507fhx9e3yrsck8zs0l3q4w" -CONTRACT_NAME_SERVICE = ( +TESTNET_PREFIX = "test-agent" +MAINNET_PREFIX = "agent" + +MAINNET_CONTRACT_ALMANAC = ( + "fetch1mezzhfj7qgveewzwzdk6lz5sae4dunpmmsjr9u7z0tpmdsae8zmquq3y0y" +) +TESTNET_CONTRACT_ALMANAC = ( + "fetch1tjagw8g8nn4cwuw00cf0m5tl4l6wfw9c0ue507fhx9e3yrsck8zs0l3q4w" +) +MAINNET_CONTRACT_NAME_SERVICE = ( + "fetch1479lwv5vy8skute5cycuz727e55spkhxut0valrcm38x9caa2x8q99ef0q" +) +TESTNET_CONTRACT_NAME_SERVICE = ( "fetch1mxz8kn3l5ksaftx8a9pj9a6prpzk2uhxnqdkwuqvuh37tw80xu6qges77l" ) REGISTRATION_FEE = 500000000000000000 @@ -25,7 +32,6 @@ class AgentNetwork(Enum): REGISTRATION_UPDATE_INTERVAL_SECONDS = 3600 REGISTRATION_RETRY_INTERVAL_SECONDS = 60 AVERAGE_BLOCK_INTERVAL = 5.7 -AGENT_NETWORK = AgentNetwork.FETCHAI_TESTNET AGENTVERSE_URL = "https://agentverse.ai" ALMANAC_API_URL = AGENTVERSE_URL + "/v1/almanac/" diff --git a/python/src/uagents/context.py b/python/src/uagents/context.py index d10203e3..1b302f9e 100644 --- a/python/src/uagents/context.py +++ b/python/src/uagents/context.py @@ -34,9 +34,10 @@ from uagents.dispatch import JsonStr, dispatcher from uagents.envelope import Envelope from uagents.models import ErrorMessage, Model -from uagents.resolver import Resolver +from uagents.resolver import Resolver, parse_identifier from uagents.storage import KeyValueStore + if TYPE_CHECKING: from uagents.protocol import Protocol @@ -134,6 +135,7 @@ class Context: def __init__( self, address: str, + identifier: str, name: Optional[str], storage: KeyValueStore, resolve: Resolver, @@ -174,6 +176,7 @@ def __init__( self.ledger = ledger self._name = name self._address = str(address) + self._identifier = str(identifier) self._resolver = resolve self._identity = identity self._queries = queries @@ -206,6 +209,16 @@ def address(self) -> str: """ return self._address + @property + def identifier(self) -> str: + """ + Get the address of the agent used for communication including the network prefix. + + Returns: + str: The agent's address and network prefix. + """ + return self._identifier + @property def logger(self) -> logging.Logger: """ @@ -374,6 +387,7 @@ async def send_raw( Returns: MsgStatus: The delivery status of the message. """ + # Check if this message is a reply if ( self._message_received is not None @@ -395,44 +409,53 @@ async def send_raw( endpoint="", ) # Check if this message is a valid interval message - if self._message_received is None and self._interval_messages: - if schema_digest not in self._interval_messages: - self._logger.exception( - f"Outgoing message {message_type} is not a valid interval message" - ) - return MsgStatus( - status=DeliveryStatus.FAILED, - detail="Invalid interval message", - destination=destination, - endpoint="", - ) - - # Handle local dispatch of messages - if dispatcher.contains(destination): - await dispatcher.dispatch( - self.address, - destination, - schema_digest, - json_message, - self._session, + if ( + self._message_received is None + and self._interval_messages + and schema_digest not in self._interval_messages + ): + self._logger.exception( + f"Outgoing message {message_type} is not a valid interval message" ) return MsgStatus( - status=DeliveryStatus.DELIVERED, - detail="Message dispatched locally", + status=DeliveryStatus.FAILED, + detail="Invalid interval message", destination=destination, endpoint="", ) - # Handle queries waiting for a response - if destination in self._queries: - self._queries[destination].set_result((json_message, schema_digest)) - del self._queries[destination] - return MsgStatus( - status=DeliveryStatus.DELIVERED, - detail="Sync message resolved", - destination=destination, - endpoint="", - ) + # Extract address from destination agent identifier if present + _, _, destination_address = parse_identifier(destination) + + if destination_address: + # Handle local dispatch of messages + if dispatcher.contains(destination_address): + await dispatcher.dispatch( + self.address, + destination_address, + schema_digest, + json_message, + self._session, + ) + return MsgStatus( + status=DeliveryStatus.DELIVERED, + detail="Message dispatched locally", + destination=destination_address, + endpoint="", + ) + + # Handle queries waiting for a response + if destination_address in self._queries: + self._queries[destination_address].set_result( + (json_message, schema_digest) + ) + del self._queries[destination_address] + return MsgStatus( + status=DeliveryStatus.DELIVERED, + detail="Sync message resolved", + destination=destination_address, + endpoint="", + ) # Resolve the destination address and endpoint ('destination' can be a name or address) destination_address, endpoints = await self._resolver.resolve(destination) diff --git a/python/src/uagents/network.py b/python/src/uagents/network.py index 74c47e61..887892b1 100644 --- a/python/src/uagents/network.py +++ b/python/src/uagents/network.py @@ -20,10 +20,10 @@ from cosmpy.aerial.wallet import LocalWallet from uagents.config import ( - AgentNetwork, - CONTRACT_ALMANAC, - CONTRACT_NAME_SERVICE, - AGENT_NETWORK, + MAINNET_CONTRACT_ALMANAC, + TESTNET_CONTRACT_ALMANAC, + MAINNET_CONTRACT_NAME_SERVICE, + TESTNET_CONTRACT_NAME_SERVICE, AVERAGE_BLOCK_INTERVAL, REGISTRATION_FEE, REGISTRATION_DENOM, @@ -33,24 +33,25 @@ logger = get_logger("network") -# Setting up the Ledger and Faucet based on Agent Network -if AGENT_NETWORK == AgentNetwork.FETCHAI_TESTNET: - _ledger = LedgerClient(NetworkConfig.fetchai_stable_testnet()) - _faucet_api = FaucetApi(NetworkConfig.fetchai_stable_testnet()) -elif AGENT_NETWORK == AgentNetwork.FETCHAI_MAINNET: - _ledger = LedgerClient(NetworkConfig.fetchai_mainnet()) -else: - raise NotImplementedError +_faucet_api = FaucetApi(NetworkConfig.fetchai_stable_testnet()) +_testnet_ledger = LedgerClient(NetworkConfig.fetchai_stable_testnet()) +_mainnet_ledger = LedgerClient(NetworkConfig.fetchai_mainnet()) -def get_ledger() -> LedgerClient: + +def get_ledger(test: bool = True) -> LedgerClient: """ Get the Ledger client. + Args: + test (bool): Whether to use the testnet or mainnet. Defaults to True. + Returns: LedgerClient: The Ledger client instance. """ - return _ledger + if test: + return _testnet_ledger + return _mainnet_ledger def get_faucet() -> FaucetApi: @@ -65,6 +66,7 @@ def get_faucet() -> FaucetApi: async def wait_for_tx_to_complete( tx_hash: str, + ledger: LedgerClient, timeout: Optional[timedelta] = None, poll_period: Optional[timedelta] = None, ) -> TxResponse: @@ -73,10 +75,10 @@ async def wait_for_tx_to_complete( Args: tx_hash (str): The hash of the transaction to monitor. - timeout (Optional[timedelta], optional): The maximum time to wait for + ledger (LedgerClient): The Ledger client to poll. + timeout (Optional[timedelta], optional): The maximum time to wait. the transaction to complete. Defaults to None. poll_period (Optional[timedelta], optional): The time interval to poll - the Ledger for the transaction status. Defaults to None. Returns: TxResponse: The response object containing the transaction details. @@ -88,7 +90,7 @@ async def wait_for_tx_to_complete( start = datetime.now() while True: try: - return _ledger.query_tx(tx_hash) + return ledger.query_tx(tx_hash) except NotFoundError: pass @@ -231,7 +233,7 @@ async def register( transaction = prepare_and_broadcast_basic_transaction( ledger, transaction, wallet ) - await wait_for_tx_to_complete(transaction.tx_hash) + await wait_for_tx_to_complete(transaction.tx_hash, ledger) def get_sequence(self, address: str) -> int: """ @@ -249,17 +251,27 @@ def get_sequence(self, address: str) -> int: return sequence -_almanac_contract = AlmanacContract(None, _ledger, CONTRACT_ALMANAC) +_mainnet_almanac_contract = AlmanacContract( + None, _mainnet_ledger, MAINNET_CONTRACT_ALMANAC +) +_testnet_almanac_contract = AlmanacContract( + None, _testnet_ledger, TESTNET_CONTRACT_ALMANAC +) -def get_almanac_contract() -> AlmanacContract: +def get_almanac_contract(test: bool = True) -> AlmanacContract: """ Get the AlmanacContract instance. + Args: + test (bool): Whether to use the testnet or mainnet. Defaults to True. + Returns: AlmanacContract: The AlmanacContract instance. """ - return _almanac_contract + if test: + return _testnet_almanac_contract + return _mainnet_almanac_contract class NameServiceContract(LedgerContract): @@ -321,7 +333,12 @@ def is_domain_public(self, domain: str): return res["is_public"] def get_registration_tx( - self, name: str, wallet_address: str, agent_address: str, domain: str + self, + name: str, + wallet_address: str, + agent_address: str, + domain: str, + test: bool, ): """ Get the registration transaction for registering a name within a domain. @@ -331,6 +348,7 @@ def get_registration_tx( wallet_address (str): The wallet address initiating the registration. agent_address (str): The address of the agent. domain (str): The domain in which the name is registered. + test (bool): The agent type Returns: Optional[Transaction]: The registration transaction, or None if the name is not @@ -348,11 +366,12 @@ def get_registration_tx( } } + contract = ( + TESTNET_CONTRACT_NAME_SERVICE if test else MAINNET_CONTRACT_NAME_SERVICE + ) transaction = Transaction() transaction.add_message( - create_cosmwasm_execute_msg( - wallet_address, CONTRACT_NAME_SERVICE, registration_msg - ) + create_cosmwasm_execute_msg(wallet_address, contract, registration_msg) ) return transaction @@ -376,8 +395,11 @@ async def register( domain (str): The domain in which the name is registered. """ logger.info("Registering name...") + chain_id = ledger.query_chain_id() - if not get_almanac_contract().is_registered(agent_address): + if not get_almanac_contract(chain_id == "dorado-1").is_registered( + agent_address + ): logger.warning( f"Agent {name} needs to be registered in almanac contract to register its name" ) @@ -390,7 +412,11 @@ async def register( return transaction = self.get_registration_tx( - name, str(wallet.address()), agent_address, domain + name, + str(wallet.address()), + agent_address, + domain, + chain_id == "dorado-1", ) if transaction is None: @@ -401,18 +427,28 @@ async def register( transaction = prepare_and_broadcast_basic_transaction( ledger, transaction, wallet ) - await wait_for_tx_to_complete(transaction.tx_hash) + await wait_for_tx_to_complete(transaction.tx_hash, ledger) logger.info("Registering name...complete") -_name_service_contract = NameServiceContract(None, _ledger, CONTRACT_NAME_SERVICE) +_mainnet_name_service_contract = NameServiceContract( + None, _mainnet_ledger, MAINNET_CONTRACT_NAME_SERVICE +) +_testnet_name_service_contract = NameServiceContract( + None, _testnet_ledger, TESTNET_CONTRACT_NAME_SERVICE +) -def get_name_service_contract() -> NameServiceContract: +def get_name_service_contract(test: bool = True) -> NameServiceContract: """ Get the NameServiceContract instance. + Args: + test (bool): Whether to use the testnet or mainnet. Defaults to True. + Returns: NameServiceContract: The NameServiceContract instance. """ - return _name_service_contract + if test: + return _testnet_name_service_contract + return _mainnet_name_service_contract diff --git a/python/src/uagents/resolver.py b/python/src/uagents/resolver.py index 63a35db8..16d4a602 100644 --- a/python/src/uagents/resolver.py +++ b/python/src/uagents/resolver.py @@ -4,11 +4,72 @@ from typing import Dict, List, Optional, Tuple import random -from uagents.config import DEFAULT_MAX_ENDPOINTS +from uagents.config import ( + DEFAULT_MAX_ENDPOINTS, + TESTNET_PREFIX, + MAINNET_PREFIX, + AGENT_PREFIX, +) from uagents.network import get_almanac_contract, get_name_service_contract -def query_record(agent_address: str, service: str) -> dict: +def is_valid_address(address: str) -> bool: + """ + Check if the given string is a valid address. + + Args: + address (str): The address to be checked. + + Returns: + bool: True if the address is valid; False otherwise. + """ + return len(address) == 65 and address.startswith(AGENT_PREFIX) + + +def is_valid_prefix(prefix: str) -> bool: + """ + Check if the given string is a valid prefix. + + Args: + prefix (str): The prefix to be checked. + + Returns: + bool: True if the prefix is valid; False otherwise. + """ + valid_prefixes = [TESTNET_PREFIX, MAINNET_PREFIX, ""] + return prefix in valid_prefixes + + +def parse_identifier(identifier: str) -> Tuple[str, str, str]: + """ + Parse an agent identifier string into prefix, name, and address. + + Args: + identifier (str): The identifier string to be parsed. + + Returns: + Tuple[str, str, str]: A tuple containing the prefix, name, and address as strings. + """ + + prefix = "" + name = "" + address = "" + + if "://" in identifier: + prefix, identifier = identifier.split("://", 1) + + if "/" in identifier: + name, identifier = identifier.split("/", 1) + + if is_valid_address(identifier): + address = identifier + else: + name = identifier + + return prefix, name, address + + +def query_record(agent_address: str, service: str, test: bool) -> dict: """ Query a record from the Almanac contract. @@ -19,7 +80,7 @@ def query_record(agent_address: str, service: str) -> dict: Returns: dict: The query result. """ - contract = get_almanac_contract() + contract = get_almanac_contract(test) query_msg = { "query_record": {"agent_address": agent_address, "record_type": service} } @@ -27,18 +88,19 @@ def query_record(agent_address: str, service: str) -> dict: return result -def get_agent_address(name: str) -> str: +def get_agent_address(name: str, test: bool) -> str: """ Get the agent address associated with the provided name from the name service contract. Args: name (str): The name to query. + test (bool): Whether to use the testnet or mainnet contract. Returns: Optional[str]: The associated agent address if found. """ query_msg = {"domain_record": {"domain": f"{name}"}} - result = get_name_service_contract().query(query_msg) + result = get_name_service_contract(test).query(query_msg) if result["record"] is not None: registered_address = result["record"]["records"][0]["agent_address"]["records"] if len(registered_address) > 0: @@ -46,25 +108,6 @@ def get_agent_address(name: str) -> str: return None -def is_agent_address(address): - """ - Check if the provided address is a valid agent address. - - Args: - address: The address to check. - - Returns: - bool: True if the address is a valid agent address, False otherwise. - """ - if not isinstance(address, str): - return False - - prefix = "agent" - expected_length = 65 - - return address.startswith(prefix) and len(address) == expected_length - - class Resolver(ABC): @abstractmethod # pylint: disable=unnecessary-pass @@ -105,9 +148,14 @@ async def resolve(self, destination: str) -> Tuple[Optional[str], List[str]]: Returns: Tuple[Optional[str], List[str]]: The address (if available) and resolved endpoints. """ - if is_agent_address(destination): - return await self._almanc_resolver.resolve(destination) - return await self._name_service_resolver.resolve(destination) + + prefix, _, address = parse_identifier(destination) + + if is_valid_prefix(prefix): + resolver = self._almanc_resolver if address else self._name_service_resolver + return await resolver.resolve(destination) + + return None, [] class AlmanacResolver(Resolver): @@ -130,7 +178,9 @@ async def resolve(self, destination: str) -> Tuple[Optional[str], List[str]]: Returns: Tuple[str, List[str]]: The address and resolved endpoints. """ - result = query_record(destination, "service") + prefix, _, address = parse_identifier(destination) + is_testnet = prefix != MAINNET_PREFIX + result = query_record(address, "service", is_testnet) if result is not None: record = result.get("record") or {} endpoint_list = ( @@ -140,7 +190,7 @@ async def resolve(self, destination: str) -> Tuple[Optional[str], List[str]]: if len(endpoint_list) > 0: endpoints = [val.get("url") for val in endpoint_list] weights = [val.get("weight") for val in endpoint_list] - return destination, random.choices( + return address, random.choices( endpoints, weights=weights, k=min(self._max_endpoints, len(endpoints)), @@ -170,7 +220,9 @@ async def resolve(self, destination: str) -> Tuple[Optional[str], List[str]]: Returns: Tuple[Optional[str], List[str]]: The address (if available) and resolved endpoints. """ - address = get_agent_address(destination) + prefix, name, _ = parse_identifier(destination) + use_testnet = prefix != MAINNET_PREFIX + address = get_agent_address(name, use_testnet) if address is not None: return await self._almanac_resolver.resolve(address) return None, [] @@ -190,7 +242,7 @@ def __init__( self._rules = rules self._max_endpoints = max_endpoints or DEFAULT_MAX_ENDPOINTS - async def resolve(self, destination: str) -> Optional[str]: + async def resolve(self, destination: str) -> Tuple[Optional[str], List[str]]: """ Resolve the destination using the provided rules. diff --git a/python/src/uagents/setup.py b/python/src/uagents/setup.py index 50d189e8..44a51ceb 100644 --- a/python/src/uagents/setup.py +++ b/python/src/uagents/setup.py @@ -12,7 +12,7 @@ def fund_agent_if_low(wallet_address: str): """ - Checks the agent's wallet balance and adds funds if it's below the registration fee. + Checks the agent's wallet balance and adds testnet funds if it's below the registration fee. Args: wallet_address (str): The wallet address of the agent. @@ -20,16 +20,18 @@ def fund_agent_if_low(wallet_address: str): Returns: None """ - ledger = get_ledger() + ledger = get_ledger(test=True) faucet = get_faucet() agent_balance = ledger.query_bank_balance(Address(wallet_address)) if agent_balance < REGISTRATION_FEE: - # Add tokens to agent's wallet - LOGGER.info("Adding funds to agent...") - faucet.get_wealth(wallet_address) - LOGGER.info("Adding funds to agent...complete") + try: + LOGGER.info("Adding testnet funds to agent...") + faucet.get_wealth(wallet_address) + LOGGER.info("Adding testnet funds to agent...complete") + except Exception as ex: + LOGGER.error(f"Failed to add testnet funds to agent: {str(ex)}") def register_agent_with_mailbox(agent: Agent, email: str): diff --git a/python/tests/test_agent_address.py b/python/tests/test_agent_address.py index c9c29dc2..c6664430 100644 --- a/python/tests/test_agent_address.py +++ b/python/tests/test_agent_address.py @@ -2,6 +2,7 @@ from uagents import Agent from uagents.crypto import Identity +from uagents.resolver import parse_identifier, is_valid_address, is_valid_prefix class TestAgentAdress(unittest.TestCase): @@ -27,6 +28,61 @@ def test_agent_generate(self): self.assertEqual(alice.address[:5] == "agent", True) self.assertEqual(len(alice.address) == 65, True) + def test_extract_valid_address(self): + valid_addresses = [ + "agent1qfl32tdwlyjatc7f9tjng6sm9y7yzapy6awx4h9rrwenputzmnv5g6skess", + "test-agent://agent1qfl32tdwlyjatc7f9tjng6sm9y7yzapy6awx4h9rrwenputzmnv5g6skess", + "agent://agent1qfl32tdwlyjatc7f9tjng6sm9y7yzapy6awx4h9rrwenputzmnv5g6skess", + "test-agent://name/agent1qfl32tdwlyjatc7f9tjng6sm9y7yzapy6awx4h9rrwenputzmnv5g6skess", + "name/agent1qfl32tdwlyjatc7f9tjng6sm9y7yzapy6awx4h9rrwenputzmnv5g6skess", + ] + + for val in valid_addresses: + prefix, name, address = parse_identifier(val) + self.assertEqual( + is_valid_address(address), + True, + ) + self.assertIn(name, {"name", ""}) + self.assertEqual( + is_valid_prefix(prefix), + True, + ) + + def test_extract_valid_name(self): + valid_names = [ + "name.domain", + "test-agent://name.domain", + "agent://name.domain", + "agent://name.domain/agent1qfl32tdwlyjatc7f9tjng6sm9y7yzapy6awx4h9rrwenputzmnv5g6skess", + "name.domain/agent1qfl32tdwlyjatc7f9tjng6sm9y7yzapy6awx4h9rrwenputzmnv5g6skess", + ] + for val in valid_names: + prefix, name, address = parse_identifier(val) + self.assertEqual(name, "name.domain") + self.assertIn( + address, + { + "agent1qfl32tdwlyjatc7f9tjng6sm9y7yzapy6awx4h9rrwenputzmnv5g6skess", + "", + }, + ) + self.assertEqual(is_valid_prefix(prefix), True) + + def test_extract_invalid_address(self): + invalid_addresses = [ + "p://other1qfl32tdwlyjatc7f9tjng6sm9y7yzapy6awx4h9rrwenputzmnv5g6skess", + "prefix://myagent1qfl32tdwlyjatc7f9tjng6sm9y7yzapy6awx4h9rrwenputzmnv5g6skes", + "other-prefix://address1qfl32tdwlyjatc7f9tjng6sm9y7yzapy6awx4h9rrwenputzmnv5g6skes", + "prefix://name/alice1qfl32tdwlyjatc7f9t6sm9y7yzapy6awx4h9rrwenputzmnv5g6skess/name", + "some-prefix://bobqfl32tdwlyjatc7f9tjng6sm9y7yzapy6awx4h9rrwenputzmnv5g6skess", + ] + + for val in invalid_addresses: + prefix, _, address = parse_identifier(val) + self.assertEqual(is_valid_address(address), False) + self.assertEqual(is_valid_prefix(prefix), False) + if __name__ == "__main__": unittest.main() diff --git a/python/tests/test_agent_registration.py b/python/tests/test_agent_registration.py index 90675a51..23df6fcc 100644 --- a/python/tests/test_agent_registration.py +++ b/python/tests/test_agent_registration.py @@ -35,7 +35,7 @@ def test_name_service_failed_ownership(self): domain = "agent" - name_service_contract = get_name_service_contract() + name_service_contract = get_name_service_contract(test=True) is_owner = name_service_contract.is_owner( agent.name, domain, str(agent.wallet.address()) @@ -58,7 +58,7 @@ async def test_name_service_registration(self): "Almanac registration failed", ) - name_service_contract = get_name_service_contract() + name_service_contract = get_name_service_contract(test=True) is_name_available = name_service_contract.is_name_available(agent.name, domain) self.assertEqual(is_name_available, True, "Agent name should be available") @@ -80,7 +80,7 @@ async def test_name_service_registration(self): ) self.assertEqual(is_owner, True, "Domain ownership failed") - query_address = get_agent_address(agent.name + "." + domain) + query_address = get_agent_address(agent.name + "." + domain, True) self.assertEqual( query_address == agent.address, True, "Service contract registration failed"