Skip to content

Commit

Permalink
chore: split out cdp wallet and cdp api actions
Browse files Browse the repository at this point in the history
  • Loading branch information
0xRAG committed Jan 31, 2025
1 parent 34f7485 commit 45b82e3
Show file tree
Hide file tree
Showing 5 changed files with 319 additions and 195 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { EvmWalletProvider } from "../../wallet-providers";
import { CdpApiActionProvider } from "./cdpApiActionProvider";
import { AddressReputationSchema, RequestFaucetFundsSchema } from "./schemas";

// Mock the entire module
jest.mock("@coinbase/coinbase-sdk");

// Get the mocked constructor
const { ExternalAddress } = jest.requireMock("@coinbase/coinbase-sdk");

describe("CDP API Action Provider Input Schemas", () => {
describe("Address Reputation Schema", () => {
it("should successfully parse valid input", () => {
const validInput = {
address: "0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83",
network: "base-mainnet",
};

const result = AddressReputationSchema.safeParse(validInput);

expect(result.success).toBe(true);
expect(result.data).toEqual(validInput);
});

it("should fail parsing invalid address", () => {
const invalidInput = {
address: "invalid-address",
network: "base-mainnet",
};
const result = AddressReputationSchema.safeParse(invalidInput);

expect(result.success).toBe(false);
});
});

describe("Request Faucet Funds Schema", () => {
it("should successfully parse with optional assetId", () => {
const validInput = {
assetId: "eth",
};

const result = RequestFaucetFundsSchema.safeParse(validInput);

expect(result.success).toBe(true);
expect(result.data).toEqual(validInput);
});

it("should successfully parse without assetId", () => {
const validInput = {};
const result = RequestFaucetFundsSchema.safeParse(validInput);

expect(result.success).toBe(true);
expect(result.data).toEqual(validInput);
});
});
});

describe("CDP API Action Provider", () => {
let actionProvider: CdpApiActionProvider;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let mockExternalAddressInstance: jest.Mocked<any>;
let mockWallet: jest.Mocked<EvmWalletProvider>;

beforeEach(() => {
// Reset all mocks before each test
jest.clearAllMocks();

actionProvider = new CdpApiActionProvider();
mockExternalAddressInstance = {
reputation: jest.fn(),
faucet: jest.fn(),
};

// Mock the constructor to return our mock instance
(ExternalAddress as jest.Mock).mockImplementation(() => mockExternalAddressInstance);

mockWallet = {
deployToken: jest.fn(),
deployContract: jest.fn(),
getAddress: jest.fn().mockReturnValue("0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83"),
getNetwork: jest.fn().mockReturnValue({ networkId: "base-sepolia" }),
} as unknown as jest.Mocked<EvmWalletProvider>;
});

describe("addressReputation", () => {
it("should successfully check address reputation", async () => {
const args = {
address: "0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83",
network: "base-mainnet",
};

mockExternalAddressInstance.reputation.mockResolvedValue("Good reputation");

const result = await actionProvider.addressReputation(args);

expect(ExternalAddress).toHaveBeenCalledWith(args.network, args.address);
expect(ExternalAddress).toHaveBeenCalledTimes(1);
expect(mockExternalAddressInstance.reputation).toHaveBeenCalled();
expect(mockExternalAddressInstance.reputation).toHaveBeenCalledTimes(1);
expect(result).toBe("Good reputation");
});

it("should handle errors when checking reputation", async () => {
const args = {
address: "0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83",
network: "base-mainnet",
};

const error = new Error("Reputation check failed");
mockExternalAddressInstance.reputation.mockRejectedValue(error);

const result = await actionProvider.addressReputation(args);

expect(ExternalAddress).toHaveBeenCalledWith(args.network, args.address);
expect(ExternalAddress).toHaveBeenCalledTimes(1);
expect(mockExternalAddressInstance.reputation).toHaveBeenCalled();
expect(mockExternalAddressInstance.reputation).toHaveBeenCalledTimes(1);
expect(result).toBe(`Error checking address reputation: ${error}`);
});
});

describe("faucet", () => {
beforeEach(() => {
mockExternalAddressInstance.faucet.mockResolvedValue({
wait: jest.fn().mockResolvedValue({
getTransactionLink: jest.fn().mockReturnValue("tx-link"),
}),
});
});

it("should successfully request faucet funds with assetId", async () => {
const args = {
assetId: "eth",
};

const result = await actionProvider.faucet(mockWallet, args);

expect(ExternalAddress).toHaveBeenCalledWith("base-sepolia", mockWallet.getAddress());
expect(ExternalAddress).toHaveBeenCalledTimes(1);
expect(mockExternalAddressInstance.faucet).toHaveBeenCalledWith("eth");
expect(mockExternalAddressInstance.faucet).toHaveBeenCalledTimes(1);
expect(result).toContain("Received eth from the faucet");
expect(result).toContain("tx-link");
});

it("should successfully request faucet funds without assetId", async () => {
const args = {};

const result = await actionProvider.faucet(mockWallet, args);

expect(ExternalAddress).toHaveBeenCalledWith("base-sepolia", mockWallet.getAddress());
expect(ExternalAddress).toHaveBeenCalledTimes(1);
expect(mockExternalAddressInstance.faucet).toHaveBeenCalledWith(undefined);
expect(mockExternalAddressInstance.faucet).toHaveBeenCalledTimes(1);
expect(result).toContain("Received ETH from the faucet");
});

it("should handle faucet errors", async () => {
const args = {};
const error = new Error("Faucet request failed");
mockExternalAddressInstance.faucet.mockRejectedValue(error);

const result = await actionProvider.faucet(mockWallet, args);

expect(result).toBe(`Error requesting faucet funds: ${error}`);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { Coinbase, ExternalAddress } from "@coinbase/coinbase-sdk";
import { z } from "zod";

import { CreateAction } from "../actionDecorator";
import { ActionProvider } from "../actionProvider";
import { Network } from "../../network";
import { CdpWalletProviderConfig, EvmWalletProvider } from "../../wallet-providers";

import { AddressReputationSchema, RequestFaucetFundsSchema } from "./schemas";

/**
* CdpApiActionProvider is an action provider for CDP API.
*
* This provider is used for any action that uses the CDP API, but does not require a CDP Wallet.
*/
export class CdpApiActionProvider extends ActionProvider<EvmWalletProvider> {
/**
* Constructor for the CdpApiActionProvider class.
*
* @param config - The configuration options for the CdpApiActionProvider.
*/
constructor(config: CdpWalletProviderConfig = {}) {
super("cdp_api", []);

if (config.apiKeyName && config.apiKeyPrivateKey) {
Coinbase.configure({ apiKeyName: config.apiKeyName, privateKey: config.apiKeyPrivateKey });
} else {
Coinbase.configureFromJson();
}
}

/**
* Check the reputation of an address.
*
* @param args - The input arguments for the action
* @returns A string containing reputation data or error message
*/
@CreateAction({
name: "address_reputation",
description: `
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-mainnet")
- address: The Ethereum address to check
`,
schema: AddressReputationSchema,
})
async addressReputation(args: z.infer<typeof AddressReputationSchema>): Promise<string> {
try {
const address = new ExternalAddress(args.network, args.address);
const reputation = await address.reputation();
return reputation.toString();
} catch (error) {
return `Error checking address reputation: ${error}`;
}
}

/**
* Requests test tokens from the faucet for the default address in the wallet.
*
* @param walletProvider - The wallet provider to request funds from.
* @param args - The input arguments for the action.
* @returns A confirmation message with transaction details.
*/
@CreateAction({
name: "request_faucet_funds",
description: `This tool will request test tokens from the faucet for the default address in the wallet. It takes the wallet and asset ID as input.
If no asset ID is provided the faucet defaults to ETH. Faucet is only allowed on 'base-sepolia' and can only provide asset ID 'eth' or 'usdc'.
You are not allowed to faucet with any other network or asset ID. If you are on another network, suggest that the user sends you some ETH
from another wallet and provide the user with your wallet details.`,
schema: RequestFaucetFundsSchema,
})
async faucet(
walletProvider: EvmWalletProvider,
args: z.infer<typeof RequestFaucetFundsSchema>,
): Promise<string> {
try {
const address = new ExternalAddress(
walletProvider.getNetwork().networkId!,
walletProvider.getAddress(),
);

const faucetTx = await address.faucet(args.assetId || undefined);

const result = await faucetTx.wait();

return `Received ${
args.assetId || "ETH"
} from the faucet. Transaction: ${result.getTransactionLink()}`;
} catch (error) {
return `Error requesting faucet funds: ${error}`;
}
}

/**
* Checks if the Cdp action provider supports the given network.
*
* @param _ - The network to check.
* @returns True if the Cdp action provider supports the network, false otherwise.
* TODO: Split out into sub providers so network support can be tighter scoped.
*/
supportsNetwork = (_: Network) => true;
}

export const cdpApiActionProvider = () => new CdpApiActionProvider();
Loading

0 comments on commit 45b82e3

Please sign in to comment.