Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dexscreener actions #314

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Dexscreener Action Provider
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { dexscreenerActionProvider } from "./dexscreenerActionProvider";

describe("DexscreenerActionProvider", () => {
const fetchMock = jest.fn();
global.fetch = fetchMock;

const provider = dexscreenerActionProvider();

beforeEach(() => {
jest.resetAllMocks().restoreAllMocks();
});

describe("getPairsByChainAndPair", () => {
it("should return token pairs when API call is successful", async () => {
const mockResponse = {
pairs: [{ chainId: "solana", pairAddress: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN" }],
};
fetchMock.mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue(mockResponse) });

const result = await provider.getPairsByChainAndPair({ chainId: "solana", pairId: "So11111111111111111111111111111111111111112,EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" });
expect(result).toEqual(JSON.stringify(mockResponse.pairs));
});

it("should throw an error when API response is not ok", async () => {
fetchMock.mockResolvedValue({ ok: false, status: 404 });
await expect(provider.getPairsByChainAndPair({ chainId: "solana", pairId: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN" }))
.rejects.toThrow("HTTP error! status: 404");
});

it("should throw an error when no pairs are found", async () => {
fetchMock.mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({ pairs: [] }) });
await expect(provider.getPairsByChainAndPair({ chainId: "solana", pairId: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN" }))
.rejects.toThrow("No pairs found for chainId: solana / pairId: JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN");
});
});

describe("getTokenPairsByTokenAddress", () => {
it("should return token pairs when API call is successful", async () => {
const mockResponse = [{ chainId: "solana", pairAddress: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN" }];
fetchMock.mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue(mockResponse) });

const result = await provider.getTokenPairsByTokenAddress({ chainId: "solana", tokenAddresses: ["So11111111111111111111111111111111111111112,EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"] });
expect(result).toEqual(JSON.stringify(mockResponse));
});

it("should throw an error when API response is not ok", async () => {
fetchMock.mockResolvedValue({ ok: false, status: 500 });
await expect(provider.getTokenPairsByTokenAddress({ chainId: "solana", tokenAddresses: ["So11111111111111111111111111111111111111112,EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"] }))
.rejects.toThrow("HTTP error! status: 500");
});
});

describe("searchPairs", () => {
it("should return token pairs matching query", async () => {
const mockResponse = {
pairs: [{ chainId: "solana", pairAddress: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN" }]
};
fetchMock.mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue(mockResponse) });

const result = await provider.searchPairs({ query: "SOL/USDC" });
expect(result).toEqual(JSON.stringify(mockResponse.pairs));
});

it("should throw an error when API response is not ok", async () => {
fetchMock.mockResolvedValue({ ok: false, status: 403 });
await expect(provider.searchPairs({ query: "SOL/USDC" }))
.rejects.toThrow("HTTP error! status: 403");
});

it("should throw an error when no token pairs are found", async () => {
fetchMock.mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({ pairs: [] }) });
await expect(provider.searchPairs({ query: "SOL/USDC" }))
.rejects.toThrow("No token pair found for SOL/USDC");
});
})
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { z } from "zod";
import { ActionProvider } from "../actionProvider";
import { CreateAction } from "../actionDecorator";
import { GetPairsByChainAndPairSchema, GetTokenPairsSchema, SearchPairsSchema } from "./schemas";

/**
* DexscreenerActionProvider is an action provider for Dexscreener.
*/
export class DexscreenerActionProvider extends ActionProvider {
/**
* Constructs a new DexscreenerActionProvider.
*/
constructor() {
super("dexscreener", []);
}

/**
* Get pairs by chainId and pairId from Dexscreener
*
* @param args - The arguments for the action.
* @returns The array of token pairs.
*/
@CreateAction({
name: "get_pairs_by_chain_and_pair",
description: "Get pairs by chainId and pairId from Dexscreener",
schema: GetPairsByChainAndPairSchema
})
async getPairsByChainAndPair(args: z.infer<typeof GetPairsByChainAndPairSchema>): Promise<string> {
const url = `https://api.dexscreener.com/latest/dex/pairs/${args.chainId}/${args.pairId}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const tokenPair = await response.json();
const pairs = tokenPair.pairs
if (pairs.length === 0) {
throw new Error(`No pairs found for chainId: ${args.chainId} / pairId: ${args.pairId}`);
}
return JSON.stringify(pairs)
}

/**
* Get all token pairs for given token addresses from Dexscreener
*
* @param args - The arguments for the action.
* @returns The array of multiple token pairs by token address.
*/
@CreateAction({
name: "get_token_pairs_by_token_address",
description: "Get all token pairs for given token addresses from Dexscreener",
schema: GetTokenPairsSchema
})
async getTokenPairsByTokenAddress(args: z.infer<typeof GetTokenPairsSchema>): Promise<string> {
const addresses = args.tokenAddresses.join(",");
const url = `https://api.dexscreener.com/tokens/v1/${args.chainId}/${addresses}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const pairs = await response.json();
return JSON.stringify(pairs)
}

/**
* Search for pairs matching a query string on Dexscreener
*
* @param args - The arguments for the action.
* @returns The array of token pairs.
*/
@CreateAction({
name: "search_pairs",
description: "Search for pairs matching a query string on Dexscreener",
schema: SearchPairsSchema
})
async searchPairs(args: z.infer<typeof SearchPairsSchema>): Promise<string> {
const url = `https://api.dexscreener.com/latest/dex/search?q=${encodeURIComponent(args.query)}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const tokenPair = await response.json();
const pairs = tokenPair.pairs
if (pairs.length === 0) {
throw new Error(`No token pair found for ${args.query}`);
}
return JSON.stringify(pairs)
}

/**
* Checks if the Dexscreener action provider supports the given network.
*
* @returns True if the Dexscreener action provider supports the network, false otherwise.
*/
supportsNetwork = () => true;

}

export const dexscreenerActionProvider = () => new DexscreenerActionProvider();
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./dexscreenerActionProvider";
export * from "./schemas";
30 changes: 30 additions & 0 deletions typescript/agentkit/src/action-providers/dexscreener/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { z } from "zod";

/**
* Input schema for get pairs by chain and pair schema action.
*/
export const GetPairsByChainAndPairSchema = z
.object({
chainId: z.string().describe("The chain ID of the pair"),
pairId: z.string().describe("The pair ID to fetch"),
})
.strict();

/**
* Input schema for get token pairs schema action.
*/
export const GetTokenPairsSchema = z
.object({
chainId: z.string().describe("The chain ID of the pair"),
tokenAddresses: z.array(z.string()).describe("List of the token addresses"),
})
.strict();

/**
* Input schema for search pairs schema action.
*/
export const SearchPairsSchema = z
.object({
query: z.string().describe("The search query string"),
})
.strict();
1 change: 1 addition & 0 deletions typescript/agentkit/src/action-providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ export * from "./wallet";
export * from "./customActionProvider";
export * from "./alchemy";
export * from "./moonwell";
export * from "./dexscreener";