forked from coinbase/agentkit
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add alchemy prices api to typescript action providers (coinbase#252)
- Loading branch information
1 parent
b3dc642
commit 8fe99da
Showing
7 changed files
with
390 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
161 changes: 161 additions & 0 deletions
161
typescript/agentkit/src/action-providers/alchemy/alchemyTokenPricesActionProvider.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |
176 changes: 176 additions & 0 deletions
176
typescript/agentkit/src/action-providers/alchemy/alchemyTokenPricesActionProvider.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from "./alchemyTokenPricesActionProvider"; | ||
export * from "./schemas"; |
Oops, something went wrong.