From 8bd0b462d2163696c52ba660aa5d7a0c094147f7 Mon Sep 17 00:00:00 2001
From: Arihant Bansal <17180950+arihantbansal@users.noreply.github.com>
Date: Sat, 4 Jan 2025 23:37:51 +0530
Subject: [PATCH] feat: update to hermes v2
---
README.md | 9 +--
.../app/api/chat/route.ts | 80 +++++++++----------
.../data/DefaultRetrievalText.ts | 2 +-
.../utils/markdownToHTML.ts | 30 +++----
package.json | 1 -
src/actions/pythFetchPrice.ts | 10 ++-
src/agent/index.ts | 16 ++--
src/langchain/index.ts | 18 +++--
src/tools/batch_order.ts | 24 +-----
src/tools/index.ts | 72 +++++++----------
src/tools/pyth_fetch_price.ts | 79 +++++++++++++-----
src/types/index.ts | 61 +++++++++++++-
12 files changed, 240 insertions(+), 162 deletions(-)
diff --git a/README.md b/README.md
index f4d5562..adeab95 100644
--- a/README.md
+++ b/README.md
@@ -198,11 +198,11 @@ import { PublicKey } from "@solana/web3.js";
```typescript
-const price = await agent.pythFetchPrice(
- "0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"
-);
+const priceFeedID = await agent.getPythPriceFeedID("SOL");
-console.log("Price in BTC/USD:", price);
+const price = await agent.getPythPrice(priceFeedID);
+
+console.log("Price of SOL/USD:", price);
```
## Examples
@@ -233,7 +233,6 @@ The toolkit relies on several key Solana and Metaplex libraries:
- @metaplex-foundation/umi
- @lightprotocol/compressed-token
- @lightprotocol/stateless.js
-- @pythnetwork/price-service-client
## Contributing
diff --git a/examples/agent-kit-nextjs-langchain/app/api/chat/route.ts b/examples/agent-kit-nextjs-langchain/app/api/chat/route.ts
index 06e6911..badd6b3 100644
--- a/examples/agent-kit-nextjs-langchain/app/api/chat/route.ts
+++ b/examples/agent-kit-nextjs-langchain/app/api/chat/route.ts
@@ -5,24 +5,24 @@ import { createReactAgent } from "@langchain/langgraph/prebuilt";
import { SolanaAgentKit, createSolanaTools } from "solana-agent-kit";
const llm = new ChatOpenAI({
- temperature: 0.7,
- model: "gpt-4o-mini",
+ temperature: 0.7,
+ model: "gpt-4o-mini",
});
const solanaAgent = new SolanaAgentKit(
- process.env.SOLANA_PRIVATE_KEY!,
- process.env.RPC_URL,
- process.env.OPENAI_API_KEY!,
+ process.env.SOLANA_PRIVATE_KEY!,
+ process.env.RPC_URL,
+ process.env.OPENAI_API_KEY!,
);
const tools = createSolanaTools(solanaAgent);
const memory = new MemorySaver();
const agent = createReactAgent({
- llm,
- tools,
- checkpointSaver: memory,
- messageModifier: `
+ llm,
+ tools,
+ checkpointSaver: memory,
+ messageModifier: `
You are a helpful agent that can interact onchain using the Solana Agent Kit. You are
empowered to interact onchain using your tools. If you ever need funds, you can request them from the
faucet. If not, you can provide your wallet details and request funds from the user. If there is a 5XX
@@ -34,38 +34,38 @@ const agent = createReactAgent({
});
export async function POST(req: NextRequest) {
- try {
- const body = await req.json();
- const messages = body.messages ?? [];
+ try {
+ const body = await req.json();
+ const messages = body.messages ?? [];
- const eventStream = agent.streamEvents(
- {
- messages,
- },
- {
- version: "v2",
- configurable: {
- thread_id: "Solana Agent Kit!",
- },
- },
- );
+ const eventStream = agent.streamEvents(
+ {
+ messages,
+ },
+ {
+ version: "v2",
+ configurable: {
+ thread_id: "Solana Agent Kit!",
+ },
+ },
+ );
- const textEncoder = new TextEncoder();
- const transformStream = new ReadableStream({
- async start(controller) {
- for await (const { event, data } of eventStream) {
- if (event === "on_chat_model_stream") {
- if (!!data.chunk.content) {
- controller.enqueue(textEncoder.encode(data.chunk.content));
- }
- }
- }
- controller.close();
- },
- });
+ const textEncoder = new TextEncoder();
+ const transformStream = new ReadableStream({
+ async start(controller) {
+ for await (const { event, data } of eventStream) {
+ if (event === "on_chat_model_stream") {
+ if (data.chunk.content) {
+ controller.enqueue(textEncoder.encode(data.chunk.content));
+ }
+ }
+ }
+ controller.close();
+ },
+ });
- return new Response(transformStream);
- } catch (e: any) {
- return NextResponse.json({ error: e.message }, { status: e.status ?? 500 });
- }
+ return new Response(transformStream);
+ } catch (e: any) {
+ return NextResponse.json({ error: e.message }, { status: e.status ?? 500 });
+ }
}
diff --git a/examples/agent-kit-nextjs-langchain/data/DefaultRetrievalText.ts b/examples/agent-kit-nextjs-langchain/data/DefaultRetrievalText.ts
index 898acba..6973d98 100644
--- a/examples/agent-kit-nextjs-langchain/data/DefaultRetrievalText.ts
+++ b/examples/agent-kit-nextjs-langchain/data/DefaultRetrievalText.ts
@@ -537,4 +537,4 @@ const executor = await initializeAgentExecutorWithOptions(tools, llm, {
},
});
\`\`\`
-`;
\ No newline at end of file
+`;
diff --git a/examples/agent-kit-nextjs-langchain/utils/markdownToHTML.ts b/examples/agent-kit-nextjs-langchain/utils/markdownToHTML.ts
index dc265b1..135fdd9 100644
--- a/examples/agent-kit-nextjs-langchain/utils/markdownToHTML.ts
+++ b/examples/agent-kit-nextjs-langchain/utils/markdownToHTML.ts
@@ -2,29 +2,29 @@ import { marked } from "marked";
import DOMPurify from "isomorphic-dompurify";
interface MarkedOptions {
- gfm: boolean;
- breaks: boolean;
- headerIds: boolean;
- mangle: false;
- highlight?: (code: string, lang: string) => string;
+ gfm: boolean;
+ breaks: boolean;
+ headerIds: boolean;
+ mangle: false;
+ highlight?: (code: string, lang: string) => string;
}
// Configure marked options
const markedOptions: MarkedOptions = {
- gfm: true, // GitHub Flavored Markdown
- breaks: true, // Convert \n to
- headerIds: true, // Add ids to headers
- mangle: false, // Don't escape HTML
- highlight: function (code: string, lang: string): string {
- // You can add syntax highlighting here if needed
- return code;
- },
+ gfm: true, // GitHub Flavored Markdown
+ breaks: true, // Convert \n to
+ headerIds: true, // Add ids to headers
+ mangle: false, // Don't escape HTML
+ highlight: function (code: string, lang: string): string {
+ // You can add syntax highlighting here if needed
+ return code;
+ },
};
marked.setOptions(markedOptions);
// Basic markdown to HTML conversion with sanitization
export default function markdownToHtml(markdown: string) {
- const rawHtml = marked.parse(markdown);
- return DOMPurify.sanitize(rawHtml as string);
+ const rawHtml = marked.parse(markdown);
+ return DOMPurify.sanitize(rawHtml as string);
}
diff --git a/package.json b/package.json
index 1ee3da7..cb6366f 100644
--- a/package.json
+++ b/package.json
@@ -39,7 +39,6 @@
"@onsol/tldparser": "^0.6.7",
"@orca-so/common-sdk": "0.6.4",
"@orca-so/whirlpools-sdk": "^0.13.12",
- "@pythnetwork/price-service-client": "^1.9.0",
"@raydium-io/raydium-sdk-v2": "0.1.95-alpha",
"@solana/spl-token": "^0.4.9",
"@tensor-oss/tensorswap-sdk": "^4.5.0",
diff --git a/src/actions/pythFetchPrice.ts b/src/actions/pythFetchPrice.ts
index 8dc11f6..3f9b9de 100644
--- a/src/actions/pythFetchPrice.ts
+++ b/src/actions/pythFetchPrice.ts
@@ -1,7 +1,7 @@
import { Action } from "../types/action";
import { SolanaAgentKit } from "../agent";
import { z } from "zod";
-import { pythFetchPrice } from "../tools";
+import { fetchPythPrice, fetchPythPriceFeedID } from "../tools";
const pythFetchPriceAction: Action = {
name: "PYTH_FETCH_PRICE",
@@ -18,7 +18,7 @@ const pythFetchPriceAction: Action = {
[
{
input: {
- priceFeedId: "Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD", // SOL/USD price feed
+ tokenSymbol: "SOL", // SOL/USD price feed
},
output: {
status: "success",
@@ -37,8 +37,10 @@ const pythFetchPriceAction: Action = {
}),
handler: async (_agent: SolanaAgentKit, input: Record) => {
try {
- const priceFeedId = input.tokenId as string;
- const priceStr = await pythFetchPrice(priceFeedId);
+ const priceFeedId = await fetchPythPriceFeedID(input.tokenSymbol);
+
+ const priceStr = await fetchPythPrice(priceFeedId);
+
return {
status: "success",
price: priceStr,
diff --git a/src/agent/index.ts b/src/agent/index.ts
index 94f94d4..a4aa87b 100644
--- a/src/agent/index.ts
+++ b/src/agent/index.ts
@@ -1,4 +1,5 @@
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
+import { BN } from "@coral-xyz/anchor";
import bs58 from "bs58";
import Decimal from "decimal.js";
import { DEFAULT_OPTIONS } from "../constants";
@@ -37,7 +38,6 @@ import {
orcaOpenSingleSidedPosition,
FEE_TIERS,
fetchPrice,
- pythFetchPrice,
getAllDomainsTLDs,
getAllRegisteredAllDomains,
getOwnedDomainsForTLD,
@@ -53,9 +53,9 @@ import {
cancelListing,
fetchTokenReportSummary,
fetchTokenDetailedReport,
- OrderParams,
+ fetchPythPrice,
+ fetchPythPriceFeedID,
} from "../tools";
-
import {
CollectionDeployment,
CollectionOptions,
@@ -64,8 +64,8 @@ import {
MintCollectionNFTResponse,
PumpfunLaunchResponse,
PumpFunTokenOptions,
+ OrderParams,
} from "../types";
-import { BN } from "@coral-xyz/anchor";
/**
* Main class for interacting with Solana blockchain
@@ -442,8 +442,12 @@ export class SolanaAgentKit {
return manifestCreateMarket(this, baseMint, quoteMint);
}
- async pythFetchPrice(priceFeedID: string): Promise {
- return pythFetchPrice(priceFeedID);
+ async getPythPriceFeedID(tokenSymbol: string): Promise {
+ return fetchPythPriceFeedID(tokenSymbol);
+ }
+
+ async getPythPrice(priceFeedID: string): Promise {
+ return fetchPythPrice(priceFeedID);
}
async createGibworkTask(
diff --git a/src/langchain/index.ts b/src/langchain/index.ts
index 38fd7ec..9f5f56b 100644
--- a/src/langchain/index.ts
+++ b/src/langchain/index.ts
@@ -1,14 +1,14 @@
import { PublicKey } from "@solana/web3.js";
+import { BN } from "@coral-xyz/anchor";
import Decimal from "decimal.js";
import { Tool } from "langchain/tools";
import {
GibworkCreateTaskReponse,
+ OrderParams,
PythFetchPriceResponse,
SolanaAgentKit,
} from "../index";
-import { create_image } from "../tools/create_image";
-import { BN } from "@coral-xyz/anchor";
-import { FEE_TIERS, generateOrdersfromPattern, OrderParams } from "../tools";
+import { create_image, FEE_TIERS, generateOrdersfromPattern } from "../tools";
export class SolanaBalanceTool extends Tool {
name = "solana_balance";
@@ -1497,7 +1497,7 @@ export class SolanaPythFetchPrice extends Tool {
description = `Fetch the price of a given price feed from Pyth's Hermes service
Inputs:
- priceFeedID: string, the price feed ID, e.g., "0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43" for BTC/USD`;
+ tokenSymbol: string, e.g., BTC for bitcoin`;
constructor(private solanaKit: SolanaAgentKit) {
super();
@@ -1505,17 +1505,21 @@ export class SolanaPythFetchPrice extends Tool {
async _call(input: string): Promise {
try {
- const price = await this.solanaKit.pythFetchPrice(input);
+ const priceFeedID = await this.solanaKit.getPythPriceFeedID(input);
+ const price = await this.solanaKit.getPythPrice(priceFeedID);
+
const response: PythFetchPriceResponse = {
status: "success",
- priceFeedID: input,
+ tokenSymbol: input,
+ priceFeedID,
price,
};
+
return JSON.stringify(response);
} catch (error: any) {
const response: PythFetchPriceResponse = {
status: "error",
- priceFeedID: input,
+ tokenSymbol: input,
message: error.message,
code: error.code || "UNKNOWN_ERROR",
};
diff --git a/src/tools/batch_order.ts b/src/tools/batch_order.ts
index f788ba4..c27fa98 100644
--- a/src/tools/batch_order.ts
+++ b/src/tools/batch_order.ts
@@ -4,33 +4,13 @@ import {
sendAndConfirmTransaction,
TransactionInstruction,
} from "@solana/web3.js";
-import { SolanaAgentKit } from "../index";
import {
ManifestClient,
WrapperPlaceOrderParamsExternal,
} from "@cks-systems/manifest-sdk";
import { OrderType } from "@cks-systems/manifest-sdk/client/ts/src/wrapper/types/OrderType";
-
-export interface OrderParams {
- quantity: number;
- side: string;
- price: number;
-}
-
-interface BatchOrderPattern {
- side: string;
- totalQuantity?: number;
- priceRange?: {
- min?: number;
- max?: number;
- };
- spacing?: {
- type: "percentage" | "fixed";
- value: number;
- };
- numberOfOrders?: number;
- individualQuantity?: number;
-}
+import { SolanaAgentKit } from "../index";
+import { BatchOrderPattern, OrderParams } from "../types";
/**
* Generates an array of orders based on the specified pattern
diff --git a/src/tools/index.ts b/src/tools/index.ts
index 9e4a355..f38e328 100644
--- a/src/tools/index.ts
+++ b/src/tools/index.ts
@@ -1,58 +1,46 @@
-import exp from "constants";
-
-export * from "./request_faucet_funds";
-export * from "./deploy_token";
-export * from "./deploy_collection";
-export * from "./get_balance";
-export * from "./get_balance_other";
-export * from "./mint_nft";
-export * from "./transfer";
-export * from "./trade";
-export * from "./limit_order";
export * from "./batch_order";
export * from "./cancel_all_orders";
-export * from "./withdraw_all";
-export * from "./register_domain";
-export * from "./resolve_sol_domain";
+export * from "./create_gibwork_task";
+export * from "./create_image";
+export * from "./create_tiplinks";
+export * from "./deploy_collection";
+export * from "./deploy_token";
+export * from "./fetch_price";
+export * from "./get_all_domains_tlds";
+export * from "./get_all_registered_all_domains";
+export * from "./get_balance";
+export * from "./get_balance_other";
+export * from "./get_main_all_domains_domain";
+export * from "./get_owned_all_domains";
+export * from "./get_owned_domains_for_tld";
export * from "./get_primary_domain";
+export * from "./get_token_data";
+export * from "./get_tps";
export * from "./launch_pumpfun_token";
export * from "./lend";
-export * from "./get_tps";
-export * from "./get_token_data";
-export * from "./stake_with_jup";
-export * from "./fetch_price";
-export * from "./send_compressed_airdrop";
+export * from "./limit_order";
+export * from "./manifest_create_market";
+export * from "./mint_nft";
+export * from "./openbook_create_market";
export * from "./orca_close_position";
export * from "./orca_create_clmm";
export * from "./orca_create_single_sided_liquidity_pool";
export * from "./orca_fetch_positions";
export * from "./orca_open_centered_position_with_liquidity";
export * from "./orca_open_single_sided_position";
-export * from "./get_all_domains_tlds";
-export * from "./get_all_registered_all_domains";
-export * from "./get_owned_domains_for_tld";
-export * from "./get_main_all_domains_domain";
-export * from "./get_owned_all_domains";
-export * from "./resolve_domain";
-
-export * from "./get_all_domains_tlds";
-export * from "./get_all_registered_all_domains";
-export * from "./get_owned_domains_for_tld";
-export * from "./get_main_all_domains_domain";
-export * from "./get_owned_all_domains";
-export * from "./resolve_domain";
-
+export * from "./pyth_fetch_price";
export * from "./raydium_create_ammV4";
export * from "./raydium_create_clmm";
export * from "./raydium_create_cpmm";
-export * from "./openbook_create_market";
-export * from "./manifest_create_market";
-export * from "./pyth_fetch_price";
-
-export * from "./create_gibwork_task";
-
+export * from "./register_domain";
+export * from "./request_faucet_funds";
+export * from "./resolve_domain";
+export * from "./resolve_sol_domain";
export * from "./rock_paper_scissor";
-export * from "./create_tiplinks";
-
-export * from "./tensor_trade";
export * from "./rugcheck";
+export * from "./send_compressed_airdrop";
+export * from "./stake_with_jup";
+export * from "./tensor_trade";
+export * from "./trade";
+export * from "./transfer";
+export * from "./withdraw_all";
diff --git a/src/tools/pyth_fetch_price.ts b/src/tools/pyth_fetch_price.ts
index 789cec8..35d619c 100644
--- a/src/tools/pyth_fetch_price.ts
+++ b/src/tools/pyth_fetch_price.ts
@@ -1,5 +1,51 @@
-import { PriceServiceConnection } from "@pythnetwork/price-service-client";
import BN from "bn.js";
+import { PythPriceFeedIDItem } from "../types";
+
+/**
+ * Fetch the price feed ID for a given token symbol from Pyth
+ * @param tokenSymbol Token symbol
+ * @returns Price feed ID
+ */
+export async function fetchPythPriceFeedID(
+ tokenSymbol: string,
+): Promise {
+ try {
+ const stableHermesServiceUrl: string = "https://hermes.pyth.network";
+
+ const response = await fetch(
+ `${stableHermesServiceUrl}/v2/price_feeds/?query=${tokenSymbol}&asset_type=crypto`,
+ );
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const data = await response.json();
+
+ if (data.length === 0) {
+ throw new Error(`No price feed found for ${tokenSymbol}`);
+ }
+
+ if (data.length > 1) {
+ const filteredData = data.filter(
+ (item: PythPriceFeedIDItem) =>
+ item.attributes.base.toLowerCase() === tokenSymbol.toLowerCase(),
+ );
+
+ if (filteredData.length === 0) {
+ throw new Error(`No price feed found for ${tokenSymbol}`);
+ }
+
+ return filteredData[0].id;
+ }
+
+ return data[0].id;
+ } catch (error: any) {
+ throw new Error(
+ `Fetching price feed ID from Pyth failed: ${error.message}`,
+ );
+ }
+}
/**
* Fetch the price of a given price feed from Pyth
@@ -9,28 +55,25 @@ import BN from "bn.js";
*
* You can find priceFeedIDs here: https://www.pyth.network/developers/price-feed-ids#stable
*/
-export async function pythFetchPrice(priceFeedID: string): Promise {
- // get Hermes service URL from https://docs.pyth.network/price-feeds/api-instances-and-providers/hermes
- const stableHermesServiceUrl: string = "https://hermes.pyth.network";
- const connection = new PriceServiceConnection(stableHermesServiceUrl);
- const feeds = [priceFeedID];
-
+export async function fetchPythPrice(feedID: string): Promise {
try {
- const currentPrice = await connection.getLatestPriceFeeds(feeds);
+ const stableHermesServiceUrl: string = "https://hermes.pyth.network";
- if (currentPrice === undefined) {
- throw new Error("Price data not available for the given token.");
+ const response = await fetch(
+ `${stableHermesServiceUrl}/v2/updates/price/latest/?ids[]=${feedID}`,
+ );
+
+ const data = await response.json();
+
+ const parsedData = data.parsed;
+
+ if (parsedData.length === 0) {
+ throw new Error(`No price data found for ${feedID}`);
}
- if (currentPrice.length === 0) {
- throw new Error("Price data not available for the given token.");
- }
+ const price = new BN(parsedData[0].price.price);
+ const exponent = new BN(parsedData[0].price.expo);
- // get price and exponent from price feed
- const price = new BN(currentPrice[0].getPriceUnchecked().price);
- const exponent = new BN(currentPrice[0].getPriceUnchecked().expo);
-
- // convert to scaled price
const scaledPrice = price.div(new BN(10).pow(exponent));
return scaledPrice.toString();
diff --git a/src/types/index.ts b/src/types/index.ts
index 1dac764..ad88ec8 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -88,7 +88,8 @@ export interface FetchPriceResponse {
export interface PythFetchPriceResponse {
status: "success" | "error";
- priceFeedID: string;
+ tokenSymbol: string;
+ priceFeedID?: string;
price?: string;
message?: string;
code?: string;
@@ -165,3 +166,61 @@ export interface TokenCheck {
}>;
score: number;
}
+
+export interface PythPriceFeedIDItem {
+ id: string;
+ attributes: {
+ asset_type: string;
+ base: string;
+ };
+}
+
+export interface PythPriceItem {
+ binary: {
+ data: string[];
+ encoding: string;
+ };
+ parsed: [
+ Array<{
+ id: string;
+ price: {
+ price: string;
+ conf: string;
+ expo: number;
+ publish_time: number;
+ };
+ ema_price: {
+ price: string;
+ conf: string;
+ expo: number;
+ publish_time: number;
+ };
+ metadata: {
+ slot: number;
+ proof_available_time: number;
+ prev_publish_time: number;
+ };
+ }>,
+ ];
+}
+
+export interface OrderParams {
+ quantity: number;
+ side: string;
+ price: number;
+}
+
+export interface BatchOrderPattern {
+ side: string;
+ totalQuantity?: number;
+ priceRange?: {
+ min?: number;
+ max?: number;
+ };
+ spacing?: {
+ type: "percentage" | "fixed";
+ value: number;
+ };
+ numberOfOrders?: number;
+ individualQuantity?: number;
+}