From 7d076c8f4cc17c079138445816d295e14941a572 Mon Sep 17 00:00:00 2001 From: Zhe <106411133+zhe-t@users.noreply.github.com> Date: Sun, 22 Dec 2024 19:27:23 +0000 Subject: [PATCH 1/5] chore: add pyth dep --- package.json | 1 + pnpm-lock.yaml | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/package.json b/package.json index 5c085fe..790b80c 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@metaplex-foundation/umi-web3js-adapters": "^0.9.2", "@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", "@solana/web3.js": "^1.95.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7aa38c3..289b940 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: '@metaplex-foundation/mpl-token-metadata': specifier: ^3.3.0 version: 3.3.0(@metaplex-foundation/umi@0.9.2) + '@metaplex-foundation/mpl-toolbox': + specifier: ^0.9.4 + version: 0.9.4(@metaplex-foundation/umi@0.9.2) '@metaplex-foundation/umi': specifier: ^0.9.2 version: 0.9.2 @@ -53,6 +56,9 @@ importers: '@orca-so/whirlpools-sdk': specifier: ^0.13.12 version: 0.13.12(@coral-xyz/anchor@0.29.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(@orca-so/common-sdk@0.6.4(@solana/spl-token@0.4.9(@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10))(@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(decimal.js@10.4.3))(@solana/spl-token@0.4.9(@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10))(@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(decimal.js@10.4.3) + '@pythnetwork/price-service-client': + specifier: ^1.9.0 + version: 1.9.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@raydium-io/raydium-sdk-v2': specifier: 0.1.95-alpha version: 0.1.95-alpha(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10) @@ -316,6 +322,13 @@ packages: '@solana/web3.js': ^1.90.0 decimal.js: ^10.4.3 + '@pythnetwork/price-service-client@1.9.0': + resolution: {integrity: sha512-SLm3IFcfmy9iMqHeT4Ih6qMNZhJEefY14T9yTlpsH2D/FE5+BaGGnfcexUifVlfH6M7mwRC4hEFdNvZ6ebZjJg==} + deprecated: This package is deprecated and is no longer maintained. Please use @pythnetwork/hermes-client instead. + + '@pythnetwork/price-service-sdk@1.8.0': + resolution: {integrity: sha512-tFZ1thj3Zja06DzPIX2dEWSi7kIfIyqreoywvw5NQ3Z1pl5OJHQGMEhxt6Li3UCGSp2ooYZS9wl8/8XfrfrNSA==} + '@raydium-io/raydium-sdk-v2@0.1.95-alpha': resolution: {integrity: sha512-+u7yxo/R1JDysTCzOuAlh90ioBe2DlM2Hbcz/tFsxP/YzmnYQzShvNjcmc0361a4zJhmlrEJfpFXW0J3kkX5vA==} @@ -592,6 +605,9 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + axios-retry@3.9.1: + resolution: {integrity: sha512-8PJDLJv7qTTMMwdnbMvrLYuvB47M81wRtxQmEdV5w4rgbTXTt+vtPkXwajOfOdSyv/wZICJOC+/UhXH4aQ/R+w==} + axios@1.7.9: resolution: {integrity: sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==} @@ -826,6 +842,10 @@ packages: resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} engines: {node: '>= 10'} + is-retry-allowed@2.2.0: + resolution: {integrity: sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==} + engines: {node: '>=10'} + isomorphic-ws@4.0.1: resolution: {integrity: sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==} peerDependencies: @@ -1126,6 +1146,9 @@ packages: trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + ts-log@2.2.7: + resolution: {integrity: sha512-320x5Ggei84AxzlXp91QkIGSw5wgaLT6GeAH0KsqDmRZdVWW2OiSeVvElVoatk3f7nicwXlElXsoFkARiGE2yg==} + ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true @@ -1576,6 +1599,24 @@ snapshots: decimal.js: 10.4.3 tiny-invariant: 1.3.3 + '@pythnetwork/price-service-client@1.9.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)': + dependencies: + '@pythnetwork/price-service-sdk': 1.8.0 + '@types/ws': 8.5.13 + axios: 1.7.9 + axios-retry: 3.9.1 + isomorphic-ws: 4.0.1(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + ts-log: 2.2.7 + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - debug + - utf-8-validate + + '@pythnetwork/price-service-sdk@1.8.0': + dependencies: + bn.js: 5.2.1 + '@raydium-io/raydium-sdk-v2@0.1.95-alpha(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10)': dependencies: '@solana/buffer-layout': 4.0.1 @@ -2016,6 +2057,11 @@ snapshots: asynckit@0.4.0: {} + axios-retry@3.9.1: + dependencies: + '@babel/runtime': 7.26.0 + is-retry-allowed: 2.2.0 + axios@1.7.9: dependencies: follow-redirects: 1.15.9 @@ -2235,10 +2281,16 @@ snapshots: ipaddr.js@2.2.0: {} + is-retry-allowed@2.2.0: {} + isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)): dependencies: ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) + isomorphic-ws@4.0.1(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)): + dependencies: + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + jayson@4.1.3(bufferutil@4.0.8)(utf-8-validate@5.0.10): dependencies: '@types/connect': 3.4.38 @@ -2522,6 +2574,8 @@ snapshots: trim-lines@3.0.1: {} + ts-log@2.2.7: {} + ts-node@10.9.2(@types/node@22.10.2)(typescript@5.7.2): dependencies: '@cspotcode/source-map-support': 0.8.1 From 761299e4f22979be341723472e938d2aec444af9 Mon Sep 17 00:00:00 2001 From: Zhe <106411133+zhe-t@users.noreply.github.com> Date: Sun, 22 Dec 2024 19:27:49 +0000 Subject: [PATCH 2/5] feat: add fetch price from pyth tool --- src/tools/index.ts | 7 ++--- src/tools/pyth_fetch_price.ts | 48 +++++++++++++++++++++++++++++++++++ src/types/index.ts | 8 ++++++ 3 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 src/tools/pyth_fetch_price.ts diff --git a/src/tools/index.ts b/src/tools/index.ts index 303cde2..eee0471 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -15,8 +15,9 @@ export * from "./get_token_data"; export * from "./stake_with_jup"; export * from "./fetch_price"; export * from "./send_compressed_airdrop"; - -export * from "./create_orca_single_sided_whirlpool";export * from "./raydium_create_ammV4"; +export * from "./create_orca_single_sided_whirlpool"; +export * from "./raydium_create_ammV4"; export * from "./raydium_create_clmm"; export * from "./raydium_create_cpmm"; -export * from "./openbook_create_market"; \ No newline at end of file +export * from "./openbook_create_market"; +export * from "./pyth_fetch_price"; \ No newline at end of file diff --git a/src/tools/pyth_fetch_price.ts b/src/tools/pyth_fetch_price.ts new file mode 100644 index 0000000..5ff7317 --- /dev/null +++ b/src/tools/pyth_fetch_price.ts @@ -0,0 +1,48 @@ +import { PublicKey } from "@solana/web3.js"; +import { SolanaAgentKit } from "../index"; +import { Tool } from "langchain/tools"; +import { PriceServiceConnection } from "@pythnetwork/price-service-client"; +import BN from "bn.js"; + +/** + * Fetch the price of a given price feed from Pyth + * @param agent SolanaAgentKit instance + * @param priceFeedID Price feed ID + * @returns Latest price value from feed + * + * You can find priceFeedIDs here: https://www.pyth.network/developers/price-feed-ids#stable + */ +export async function pythFetchPrice( + agent: SolanaAgentKit, + priceFeedID: string +) { + // 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]; + + try { + const currentPrice = await connection.getLatestPriceFeeds(feeds); + + if (currentPrice === undefined) { + throw new Error("Price data not available for the given token."); + } + + if (currentPrice.length === 0) { + throw new Error("Price data not available for the given token."); + } + + // get price and exponent from price feed + let price = new BN(currentPrice[0].getPriceUnchecked().price); + let exponent = new BN(currentPrice[0].getPriceUnchecked().expo); + + // convert to scaled price + let scaledPrice = price.div(new BN(10).pow(exponent)); + + return scaledPrice.toString(); + } catch (error: any) { + throw new Error(`Fetching price from Pyth failed: ${error.message}`); + } +} + + diff --git a/src/types/index.ts b/src/types/index.ts index 28d64cb..6c9dc16 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -77,3 +77,11 @@ export interface FetchPriceResponse { message?: string; code?: string; } + +export interface PythFetchPriceResponse { + status: "success" | "error"; + priceFeedID: string; + price?: string; + message?: string; + code?: string; +} From c675e18648937563c94fe947eec64072592d57d9 Mon Sep 17 00:00:00 2001 From: Zhe <106411133+zhe-t@users.noreply.github.com> Date: Sun, 22 Dec 2024 19:28:49 +0000 Subject: [PATCH 3/5] feat: add tool implementation --- src/agent/index.ts | 7 ++++++- src/langchain/index.ts | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/agent/index.ts b/src/agent/index.ts index 721e464..6b79575 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -25,7 +25,8 @@ import { stakeWithJup, sendCompressedAirdrop, createOrcaSingleSidedWhirlpool, - FEE_TIERS + FEE_TIERS, + pythFetchPrice } from "../tools"; import { CollectionOptions, PumpFunTokenOptions } from "../types"; import { BN } from "@coral-xyz/anchor"; @@ -270,4 +271,8 @@ export class SolanaAgentKit { tickSize, ) } + + async pythFetchPrice(priceFeedID: string) { + return pythFetchPrice(this, priceFeedID); + } } diff --git a/src/langchain/index.ts b/src/langchain/index.ts index 930b0bf..afb214f 100644 --- a/src/langchain/index.ts +++ b/src/langchain/index.ts @@ -1,7 +1,7 @@ import { PublicKey } from "@solana/web3.js"; import Decimal from "decimal.js"; import { Tool } from "langchain/tools"; -import { SolanaAgentKit } from "../index"; +import { PythFetchPriceResponse, SolanaAgentKit } from "../index"; import { create_image } from "../tools/create_image"; import { fetchPrice } from "../tools/fetch_price"; import { BN } from "@coral-xyz/anchor"; @@ -980,6 +980,38 @@ export class SolanaOpenbookCreateMarket extends Tool { } } +export class SolanaPythFetchPrice extends Tool { + name = "solana_pyth_fetch_price"; + 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`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + async _call(input: string): Promise { + try { + const price = await this.solanaKit.pythFetchPrice(input); + let response: PythFetchPriceResponse = { + status: "success", + priceFeedID: input, + price: price, + }; + return JSON.stringify(response); + } catch (error: any) { + let response: PythFetchPriceResponse = { + status: "error", + priceFeedID: input, + message: error.message, + code: error.code || "UNKNOWN_ERROR", + }; + return JSON.stringify(response); + } + } +} + export function createSolanaTools(solanaKit: SolanaAgentKit) { return [ new SolanaBalanceTool(solanaKit), @@ -1007,5 +1039,7 @@ export function createSolanaTools(solanaKit: SolanaAgentKit) { new SolanaRaydiumCreateCpmm(solanaKit), new SolanaOpenbookCreateMarket(solanaKit), new SolanaCreateSingleSidedWhirlpoolTool(solanaKit), + new SolanaPythFetchPrice(solanaKit), ]; } + From 6dc0478e2c21752ce9918e1487ccd0f640fb1764 Mon Sep 17 00:00:00 2001 From: Zhe <106411133+zhe-t@users.noreply.github.com> Date: Sun, 22 Dec 2024 19:29:11 +0000 Subject: [PATCH 4/5] feat: add pyth example to readme --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 98708ec..4d85a43 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,19 @@ import { PublicKey } from "@solana/web3.js"; })(); ``` +### Fetch Price Data from Pyth + +```typescript +import { pythFetchPrice } from "solana-agent-kit"; + +const price = await pythFetchPrice( + agent, + "0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43" +); + +console.log("Price in BTC/USD:", price); +``` + ## API Reference ### Core Functions @@ -247,6 +260,10 @@ Stake SOL with Jupiter to earn rewards. Send an SPL token airdrop to many recipients at low cost via ZK Compression. +#### `pythFetchPrice(agent, priceFeedID)` + +Fetch price data from Pyth's Hermes service. + ## Dependencies The toolkit relies on several key Solana and Metaplex libraries: @@ -258,6 +275,7 @@ The toolkit relies on several key Solana and Metaplex libraries: - @metaplex-foundation/umi - @lightprotocol/compressed-token - @lightprotocol/stateless.js +- @pythnetwork/price-service-client ## Contributing From c11b3b4596d477fae43a9372d0bbb7fe7fd7faf1 Mon Sep 17 00:00:00 2001 From: Zhe <106411133+zhe-t@users.noreply.github.com> Date: Sun, 22 Dec 2024 19:29:31 +0000 Subject: [PATCH 5/5] chore: fix add your own tool docs --- guides/add_your_own_tool.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/guides/add_your_own_tool.md b/guides/add_your_own_tool.md index 63d1172..803198c 100644 --- a/guides/add_your_own_tool.md +++ b/guides/add_your_own_tool.md @@ -19,7 +19,7 @@ Create a new TypeScript file in the `src/tools/` directory for your tool (e.g., ### 2. Implement the Tool Class -```typescript:src/tools/custom_tool.ts +```typescript:src/langchain/index.ts import { Tool } from "langchain/tools"; import { SolanaAgentKit } from "../agent"; @@ -86,6 +86,8 @@ export function createSolanaTools(agent: SolanaAgentKit) { ### 6. Usage Example +Add a code example in the `README.md` file. + ```typescript import { SolanaAgentKit, createSolanaTools } from "solana-agent-kit";