From f07065085d65fdfcf9f142486c5dcfc78ca16e96 Mon Sep 17 00:00:00 2001 From: Christopher Gerber Date: Thu, 16 Jan 2025 23:12:43 -0800 Subject: [PATCH] first pass implementing address reputation action --- .../cdp_agentkit_core/actions/__init__.py | 2 + .../actions/address_reputation.py | 64 +++++++++++ .../tests/actions/test_address_reputation.py | 93 ++++++++++++++++ .../src/actions/cdp/address_reputation.ts | 56 ++++++++++ .../typescript/src/actions/cdp/index.ts | 3 + .../src/tests/address_reputation_test.ts | 100 ++++++++++++++++++ 6 files changed, 318 insertions(+) create mode 100644 cdp-agentkit-core/python/cdp_agentkit_core/actions/address_reputation.py create mode 100644 cdp-agentkit-core/python/tests/actions/test_address_reputation.py create mode 100644 cdp-agentkit-core/typescript/src/actions/cdp/address_reputation.ts create mode 100644 cdp-agentkit-core/typescript/src/tests/address_reputation_test.ts diff --git a/cdp-agentkit-core/python/cdp_agentkit_core/actions/__init__.py b/cdp-agentkit-core/python/cdp_agentkit_core/actions/__init__.py index 7e45abdda..51a9ad403 100644 --- a/cdp-agentkit-core/python/cdp_agentkit_core/actions/__init__.py +++ b/cdp-agentkit-core/python/cdp_agentkit_core/actions/__init__.py @@ -1,4 +1,5 @@ from cdp_agentkit_core.actions.cdp_action import CdpAction +from cdp_agentkit_core.actions.address_reputation import AddressReputationAction from cdp_agentkit_core.actions.deploy_nft import DeployNftAction from cdp_agentkit_core.actions.deploy_token import DeployTokenAction from cdp_agentkit_core.actions.get_balance import GetBalanceAction @@ -31,6 +32,7 @@ def get_all_cdp_actions() -> list[type[CdpAction]]: __all__ = [ "CDP_ACTIONS", "CdpAction", + "AddressReputationAction", "DeployNftAction", "DeployTokenAction", "GetBalanceAction", diff --git a/cdp-agentkit-core/python/cdp_agentkit_core/actions/address_reputation.py b/cdp-agentkit-core/python/cdp_agentkit_core/actions/address_reputation.py new file mode 100644 index 000000000..6ba79d514 --- /dev/null +++ b/cdp-agentkit-core/python/cdp_agentkit_core/actions/address_reputation.py @@ -0,0 +1,64 @@ +from collections.abc import Callable + +from cdp import Address +from pydantic import BaseModel, Field, field_validator +import re + +from cdp_agentkit_core.actions import CdpAction + +# TODO: ask John what he thinks about standardizing responses to be the json API responses +ADDRESS_REPUTATION_PROMPT = """ +This tool checks the reputation of an address on a given network. It takes: + +- network: The network the address is on (e.g. "base-sepolia") +- address: The Ethereum address to check + +and returns: +""" + + +class AddressReputationInput(BaseModel): + """Input argument schema for checking address reputation.""" + + address: str = Field( + ..., + description="The Ethereum address to check", + ) + + network: str = Field( + ..., + description="The network to check the address on", + ) + + @field_validator("address") + def validate_address(cls, v: str) -> str: + if not re.match(r"^0x[a-fA-F0-9]{40}$", v): + raise ValueError("Invalid Ethereum address format") + return v + + +def check_address_reputation(address: str, network: str) -> str: + """Check the reputation of an address. + + Args: + address (str): The Ethereum address to check + network (str): The network the address is on + + Returns: + str: A string containing the reputation json data or error message + """ + try: + address = Address(network, address) + reputation = address.reputation() + return str(reputation) + except Exception as e: + return f"Error checking address reputation: {e!s}" + + +class AddressReputationAction(CdpAction): + """Address reputation check action.""" + + name: str = "address_reputation" + description: str = ADDRESS_REPUTATION_PROMPT + args_schema: type[BaseModel] | None = AddressReputationInput + func: Callable[..., str] = check_address_reputation diff --git a/cdp-agentkit-core/python/tests/actions/test_address_reputation.py b/cdp-agentkit-core/python/tests/actions/test_address_reputation.py new file mode 100644 index 000000000..24e3af239 --- /dev/null +++ b/cdp-agentkit-core/python/tests/actions/test_address_reputation.py @@ -0,0 +1,93 @@ +from unittest.mock import patch + +import pytest + +from cdp.address_reputation import AddressReputation, AddressReputationMetadata, AddressReputationModel +from cdp_agentkit_core.actions.address_reputation import ( + AddressReputationAction, + AddressReputationInput, + check_address_reputation, +) + +MOCK_ADDRESS = "0x1234567890123456789012345678901234567890" +MOCK_NETWORK = "base-sepolia" + + +def test_address_reputation_action_initialization(): + """Test AddressReputationAction initialization and attributes.""" + action = AddressReputationAction() + + assert action.name == "address_reputation" + assert action.args_schema == AddressReputationInput + assert callable(action.func) + + +def test_address_reputation_input_model_valid(): + """Test AddressReputationInput accepts valid parameters.""" + valid_input = AddressReputationInput( + network=MOCK_NETWORK, + address=MOCK_ADDRESS, + ) + assert valid_input.network == MOCK_NETWORK + assert valid_input.address == MOCK_ADDRESS + + +def test_address_reputation_input_model_missing_params(): + """Test AddressReputationInput raises error when params are missing.""" + with pytest.raises(ValueError): + AddressReputationInput() + + +def test_address_reputation_input_model_invalid_address(): + """Test AddressReputationInput raises error with invalid address format.""" + with pytest.raises(ValueError, match="Invalid Ethereum address format"): + AddressReputationInput( + network=MOCK_NETWORK, + address="not_an_address" + ) + + +def test_address_reputation_success(address_factory): + """Test successful address reputation check.""" + # Create the model and reputation instances + mock_model = AddressReputationModel( + score=85, + metadata=AddressReputationMetadata( + total_transactions=150, + unique_days_active=30, + longest_active_streak=10, + current_active_streak=5, + activity_period_days=45, + token_swaps_performed=20, + bridge_transactions_performed=5, + lend_borrow_stake_transactions=10, + ens_contract_interactions=2, + smart_contract_deployments=1 + ) + ) + mock_reputation = AddressReputation(model=mock_model) + + with patch('cdp_agentkit_core.actions.address_reputation.Address') as MockAddress: + mock_address_instance = MockAddress.return_value + mock_address_instance.reputation.return_value = mock_reputation + + action_response = check_address_reputation(MOCK_ADDRESS, MOCK_NETWORK) + expected_response = str(mock_reputation) + + MockAddress.assert_called_once_with(MOCK_NETWORK, MOCK_ADDRESS) + mock_address_instance.reputation.assert_called_once() + assert action_response == expected_response + + +def test_address_reputation_failure(address_factory): + """Test address reputation check failure.""" + with patch('cdp_agentkit_core.actions.address_reputation.Address') as MockAddress: + mock_address_instance = MockAddress.return_value + mock_address_instance.reputation.side_effect = Exception("API error") + + action_response = check_address_reputation(MOCK_ADDRESS, MOCK_NETWORK) + expected_response = "Error checking address reputation: API error" + + MockAddress.assert_called_once_with(MOCK_NETWORK, MOCK_ADDRESS) + mock_address_instance.reputation.assert_called_once() + assert action_response == expected_response diff --git a/cdp-agentkit-core/typescript/src/actions/cdp/address_reputation.ts b/cdp-agentkit-core/typescript/src/actions/cdp/address_reputation.ts new file mode 100644 index 000000000..3f7b79612 --- /dev/null +++ b/cdp-agentkit-core/typescript/src/actions/cdp/address_reputation.ts @@ -0,0 +1,56 @@ +import { Wallet, Address } from "@coinbase/coinbase-sdk"; +import { z } from "zod"; + +import { CdpAction } from "./cdp_action"; + +const ADDRESS_REPUTATION_PROMPT = ` +This tool checks the reputation of an address on a given network. It takes: + +- network: The network to check the address on (e.g. "base-sepolia") +- address: The Ethereum address to check +`; + +/** + * Input schema for address reputation check. + */ +export const AddressReputationInput = z + .object({ + network: z + .string() + .describe("The network to check the address on"), + address: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum address format") + .describe("The Ethereum address to check"), + }) + .strip() + .describe("Input schema for address reputation check"); + +/** + * Check the reputation of an address. + * + * @param wallet - The wallet instance + * @param args - The input arguments for the action + * @returns A string containing reputation data or error message + */ +export async function checkAddressReputation( + args: z.infer, +): Promise { + try { + const address = new Address(args.address, args.network); + const reputation = await address.reputation(); + return reputation.toString(); + } catch (error) { + return `Error checking address reputation: ${error}`; + } +} + +/** + * Address reputation check action. + */ +export class AddressReputationAction implements CdpAction { + public name = "address_reputation"; + public description = ADDRESS_REPUTATION_PROMPT; + public argsSchema = AddressReputationInput; + public func = checkAddressReputation; +} diff --git a/cdp-agentkit-core/typescript/src/actions/cdp/index.ts b/cdp-agentkit-core/typescript/src/actions/cdp/index.ts index 43f79a24b..65edc3ad5 100644 --- a/cdp-agentkit-core/typescript/src/actions/cdp/index.ts +++ b/cdp-agentkit-core/typescript/src/actions/cdp/index.ts @@ -1,4 +1,5 @@ import { CdpAction, CdpActionSchemaAny } from "./cdp_action"; +import { AddressReputationAction } from "./address_reputation"; import { DeployNftAction } from "./deploy_nft"; import { DeployTokenAction } from "./deploy_token"; import { GetBalanceAction } from "./get_balance"; @@ -21,6 +22,7 @@ import { WOW_ACTIONS } from "./defi/wow"; */ export function getAllCdpActions(): CdpAction[] { return [ + new AddressReputationAction(), new GetWalletDetailsAction(), new DeployNftAction(), new DeployTokenAction(), @@ -41,6 +43,7 @@ export const CDP_ACTIONS = getAllCdpActions().concat(WOW_ACTIONS); export { CdpAction, CdpActionSchemaAny, + AddressReputationAction, GetWalletDetailsAction, DeployNftAction, DeployTokenAction, diff --git a/cdp-agentkit-core/typescript/src/tests/address_reputation_test.ts b/cdp-agentkit-core/typescript/src/tests/address_reputation_test.ts new file mode 100644 index 000000000..817791982 --- /dev/null +++ b/cdp-agentkit-core/typescript/src/tests/address_reputation_test.ts @@ -0,0 +1,100 @@ +import { Wallet, Address } from "@coinbase/coinbase-sdk"; +import { AddressReputationAction, AddressReputationInput } from "../actions/cdp/address_reputation"; + +const MOCK_ADDRESS = "0x1234567890123456789012345678901234567890"; +const MOCK_NETWORK = "base-sepolia"; + +jest.mock("@coinbase/coinbase-sdk", () => ({ + Address: jest.fn() +})); + +describe("Address Reputation Input", () => { + const action = new AddressReputationAction(); + + it("should successfully parse valid input", () => { + const validInput = { + network: MOCK_NETWORK, + address: MOCK_ADDRESS, + }; + + const result = action.argsSchema.safeParse(validInput); + + expect(result.success).toBe(true); + expect(result.data).toEqual(validInput); + }); + + it("should fail parsing empty input", () => { + const emptyInput = {}; + const result = action.argsSchema.safeParse(emptyInput); + + expect(result.success).toBe(false); + }); + + it("should fail with invalid address", () => { + const invalidInput = { + network: MOCK_NETWORK, + address: "not_an_address", + }; + const result = action.argsSchema.safeParse(invalidInput); + + expect(result.success).toBe(false); + }); +}); + +describe("Address Reputation Action", () => { + let mockAddress: jest.Mocked
; + + beforeEach(() => { + mockAddress = { + reputation: jest.fn(), + } as unknown as jest.Mocked
; + + (Address as unknown as jest.Mock).mockImplementation(() => mockAddress); + }); + + it("should successfully check address reputation", async () => { + const mockReputation = { + score: 85, + metadata: { + total_transactions: 150, + unique_days_active: 30, + longest_active_streak: 10, + current_active_streak: 5, + activity_period_days: 45, + token_swaps_performed: 20, + bridge_transactions_performed: 5, + lend_borrow_stake_transactions: 10, + ens_contract_interactions: 2, + smart_contract_deployments: 1 + }, + toString: () => "Address Reputation: (score=85, metadata=(...))" + }; + + mockAddress.reputation.mockResolvedValue(mockReputation as any); + + const args = { + network: MOCK_NETWORK, + address: MOCK_ADDRESS, + }; + + const action = new AddressReputationAction(); + const response = await action.func(args); + + expect(response).toBe(mockReputation.toString()); + }); + + it("should handle errors gracefully", async () => { + const error = new Error("API error"); + mockAddress.reputation.mockRejectedValue(error); + + const args = { + network: MOCK_NETWORK, + address: MOCK_ADDRESS, + }; + + const action = new AddressReputationAction(); + const response = await action.func(args); + + expect(response).toBe(`Error checking address reputation: ${error}`); + }); +});