Skip to content

Commit

Permalink
add alchemy prices api to typescript action providers (coinbase#252)
Browse files Browse the repository at this point in the history
  • Loading branch information
SahilAujla authored Feb 4, 2025
1 parent b3dc642 commit 8fe99da
Show file tree
Hide file tree
Showing 7 changed files with 390 additions and 0 deletions.
6 changes: 6 additions & 0 deletions typescript/agentkit/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased

### Added

- Added `alchemyTokenPricesActionProvider` to fetch token prices from Alchemy.
- Added `token_prices_by_symbol` action to fetch token prices by symbol.
- Added `token_prices_by_address` action to fetch token prices by network and address pairs.

## [0.1.1] - 2025-02-02

### Added
Expand Down
9 changes: 9 additions & 0 deletions typescript/agentkit/src/action-providers/alchemy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Alchemy

## Alchemy Token Prices Action Provider

The Alchemy Token Prices Action Provider is a tool that allows you to fetch token prices from the Alchemy API using token symbols or contract addresses for 1000+ tokens by market cap.

**Please remember to add the `ALCHEMY_API_KEY` environment variable to your `.env` file.**

Obtain an API key from [Alchemy Dashboard](https://bit.ly/agentkit-alchemy)
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import {
alchemyTokenPricesActionProvider,
AlchemyTokenPricesActionProvider,
} from "./alchemyTokenPricesActionProvider";

const MOCK_API_KEY = "alch-demo";

// Sample responses for each action
const MOCK_TOKEN_PRICES_BY_SYMBOL_RESPONSE = {
data: [
{
symbol: "ETH",
prices: [
{
currency: "usd",
value: "2873.490923459",
lastUpdatedAt: "2025-02-03T23:46:40Z",
},
],
},
],
};

const MOCK_TOKEN_PRICES_BY_ADDRESS_RESPONSE = {
data: [
{
network: "eth-mainnet",
address: "0x1234567890abcdef",
prices: [
{
currency: "usd",
value: "1234.56",
lastUpdatedAt: "2025-02-03T23:46:40Z",
},
],
},
],
};

describe("AlchemyTokenPricesActionProvider", () => {
let provider: AlchemyTokenPricesActionProvider;

beforeEach(() => {
process.env.ALCHEMY_API_KEY = MOCK_API_KEY;
provider = alchemyTokenPricesActionProvider({ apiKey: MOCK_API_KEY });
jest.restoreAllMocks();
});

afterEach(() => {
jest.clearAllMocks();
});

describe("tokenPricesBySymbol", () => {
it("should successfully fetch token prices by symbol", async () => {
const fetchMock = jest.spyOn(global, "fetch").mockResolvedValue({
ok: true,
json: async () => MOCK_TOKEN_PRICES_BY_SYMBOL_RESPONSE,
} as Response);

const response = await provider.tokenPricesBySymbol({ symbols: ["ETH", "BTC"] });

// Verify the URL has the correct API key and query parameters.
const expectedUrlPart = `${provider["baseUrl"]}/${MOCK_API_KEY}/tokens/by-symbol`;
expect(fetchMock).toHaveBeenCalled();
const calledUrl = fetchMock.mock.calls[0][0] as string;
expect(calledUrl).toContain(expectedUrlPart);
expect(calledUrl).toContain("symbols=ETH");
expect(calledUrl).toContain("symbols=BTC");

expect(response).toContain("Successfully fetched token prices by symbol");
expect(response).toContain(JSON.stringify(MOCK_TOKEN_PRICES_BY_SYMBOL_RESPONSE, null, 2));
});

it("should handle non-ok response for token prices by symbol", async () => {
jest.spyOn(global, "fetch").mockResolvedValue({
ok: false,
status: 400,
} as Response);

const response = await provider.tokenPricesBySymbol({ symbols: ["ETH"] });
expect(response).toContain("Error fetching token prices by symbol");
expect(response).toContain("400");
});

it("should handle fetch error for token prices by symbol", async () => {
const error = new Error("Fetch error");
jest.spyOn(global, "fetch").mockRejectedValue(error);

const response = await provider.tokenPricesBySymbol({ symbols: ["ETH"] });
expect(response).toContain("Error fetching token prices by symbol");
expect(response).toContain(error.message);
});
});

describe("tokenPricesByAddress", () => {
it("should successfully fetch token prices by address", async () => {
const fetchMock = jest.spyOn(global, "fetch").mockResolvedValue({
ok: true,
json: async () => MOCK_TOKEN_PRICES_BY_ADDRESS_RESPONSE,
} as Response);

const payload = {
addresses: [{ network: "eth-mainnet", address: "0x1234567890abcdef" }],
};

const response = await provider.tokenPricesByAddress(payload);
expect(fetchMock).toHaveBeenCalled();

// Verify that fetch was called with the correct POST URL and options.
const expectedUrl = `${provider["baseUrl"]}/${MOCK_API_KEY}/tokens/by-address`;
expect(fetchMock).toHaveBeenCalledWith(
expectedUrl,
expect.objectContaining({
method: "POST",
headers: expect.objectContaining({
Accept: "application/json",
"Content-Type": "application/json",
}),
body: JSON.stringify(payload),
}),
);

expect(response).toContain("Successfully fetched token prices by address");
expect(response).toContain(JSON.stringify(MOCK_TOKEN_PRICES_BY_ADDRESS_RESPONSE, null, 2));
});

it("should handle non-ok response for token prices by address", async () => {
jest.spyOn(global, "fetch").mockResolvedValue({
ok: false,
status: 429,
} as Response);

const payload = {
addresses: [{ network: "eth-mainnet", address: "0x1234567890abcdef" }],
};

const response = await provider.tokenPricesByAddress(payload);
expect(response).toContain("Error fetching token prices by address");
expect(response).toContain("429");
});

it("should handle fetch error for token prices by address", async () => {
const error = new Error("Fetch error");
jest.spyOn(global, "fetch").mockRejectedValue(error);

const payload = {
addresses: [{ network: "eth-mainnet", address: "0x1234567890abcdef" }],
};

const response = await provider.tokenPricesByAddress(payload);
expect(response).toContain("Error fetching token prices by address");
expect(response).toContain(error.message);
});
});

describe("supportsNetwork", () => {
it("should always return true", () => {
expect(provider.supportsNetwork()).toBe(true);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { z } from "zod";
import { ActionProvider } from "../actionProvider";
import { CreateAction } from "../actionDecorator";
import { AlchemyTokenPricesBySymbolSchema, AlchemyTokenPricesByAddressSchema } from "./schemas";

/**
* Configuration options for the AlchemyTokenPricesActionProvider.
*/
export interface AlchemyTokenPricesActionProviderConfig {
/**
* Alchemy API Key
*/
apiKey?: string;
}

/**
* AlchemyTokenPricesActionProvider is an action provider for fetching token prices via the Alchemy Prices API.
* This provider enables querying current and historical token prices using symbols or addresses.
*
*/
export class AlchemyTokenPricesActionProvider extends ActionProvider {
private readonly apiKey: string;
private readonly baseUrl: string;

/**
* Creates a new instance of AlchemyTokenPricesActionProvider
*
* @param config - Configuration options including the API key
*/
constructor(config: AlchemyTokenPricesActionProviderConfig = {}) {
super("alchemyTokenPrices", []);

config.apiKey ||= process.env.ALCHEMY_API_KEY;
if (!config.apiKey) {
throw new Error("ALCHEMY_API_KEY is not configured.");
}
this.apiKey = config.apiKey;
this.baseUrl = "https://api.g.alchemy.com/prices/v1";
}

/**
* Fetch current token prices for one or more token symbols.
*
* @param args - The arguments containing an array of token symbols.
* @returns A JSON string with the token prices or an error message.
*/
@CreateAction({
name: "token_prices_by_symbol",
description: `
This tool will fetch current prices for one or more tokens using their symbols via the Alchemy Prices API.
A successful response will return a JSON payload similar to:
{
"data": [
{
"symbol": "ETH",
"prices": [
{
"currency": "usd",
"value": "2873.490923459",
"lastUpdatedAt": "2025-02-03T23:46:40Z"
}
]
}
]
}
A failure response will return an error message with details.
`,
schema: AlchemyTokenPricesBySymbolSchema,
})
async tokenPricesBySymbol(
args: z.infer<typeof AlchemyTokenPricesBySymbolSchema>,
): Promise<string> {
try {
// Build query parameters: for each symbol add a separate query parameter
const params = new URLSearchParams();
for (const symbol of args.symbols) {
params.append("symbols", symbol);
}

const url = `${this.baseUrl}/${this.apiKey}/tokens/by-symbol?${params.toString()}`;
const response = await fetch(url, {
method: "GET",
headers: {
Accept: "application/json",
},
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();
return `Successfully fetched token prices by symbol:\n${JSON.stringify(data, null, 2)}`;
} catch (error) {
return `Error fetching token prices by symbol: ${error}`;
}
}

/**
* Fetch current token prices for one or more tokens identified by network and address pairs.
*
* @param args - The arguments containing an array of token network/address pairs.
* @returns A JSON string with the token prices or an error message.
*/
@CreateAction({
name: "token_prices_by_address",
description: `
This tool will fetch current prices for tokens using network and address pairs via the Alchemy Prices API.
A successful response will return a JSON payload similar to:
{
"data": [
{
"network": "eth-mainnet",
"address": "0xYourTokenAddress",
"prices": [
{
"currency": "usd",
"value": "1234.56",
"lastUpdatedAt": "2025-02-03T23:46:40Z"
}
]
}
]
}
A failure response will return an error message with details.
`,
schema: AlchemyTokenPricesByAddressSchema,
})
async tokenPricesByAddress(
args: z.infer<typeof AlchemyTokenPricesByAddressSchema>,
): Promise<string> {
try {
const url = `${this.baseUrl}/${this.apiKey}/tokens/by-address`;
const response = await fetch(url, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(args),
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();
return `Successfully fetched token prices by address:\n${JSON.stringify(data, null, 2)}`;
} catch (error) {
return `Error fetching token prices by address: ${error}`;
}
}

/**
* Checks if the Alchemy Prices action provider supports the given network.
* Since the API works with multiple networks, this always returns true.
*
* @returns Always returns true.
*/
supportsNetwork(): boolean {
return true;
}
}

/**
* Factory function to create a new AlchemyTokenPricesActionProvider instance.
*
* @param config - The configuration options for the provider.
* @returns A new instance of AlchemyTokenPricesActionProvider.
*/
export const alchemyTokenPricesActionProvider = (config?: AlchemyTokenPricesActionProviderConfig) =>
new AlchemyTokenPricesActionProvider(config);
2 changes: 2 additions & 0 deletions typescript/agentkit/src/action-providers/alchemy/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./alchemyTokenPricesActionProvider";
export * from "./schemas";
Loading

0 comments on commit 8fe99da

Please sign in to comment.