diff --git a/README.md b/README.md index 93c262c..99dae4d 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,16 @@ import { PublicKey } from "@solana/web3.js"; })(); ``` +### Fetch Price Data from Pyth + +```typescript + +const price = await agent.pythFetchPrice( + "0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43" +); + +console.log("Price in BTC/USD:", price); +``` ## Dependencies @@ -192,6 +202,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 diff --git a/guides/add_your_own_tool.md b/guides/add_your_own_tool.md index a02aa60..6c41606 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"; diff --git a/package.json b/package.json index ebf8984..b8ba342 100644 --- a/package.json +++ b/package.json @@ -28,11 +28,13 @@ "@lightprotocol/stateless.js": "^0.17.1", "@metaplex-foundation/mpl-core": "^1.1.1", "@metaplex-foundation/mpl-token-metadata": "^3.3.0", + "@metaplex-foundation/mpl-toolbox": "^0.9.4", "@metaplex-foundation/umi": "^0.9.2", "@metaplex-foundation/umi-bundle-defaults": "^0.9.2", "@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 diff --git a/src/agent/index.ts b/src/agent/index.ts index 44eae4f..691cf5d 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -25,8 +25,9 @@ import { stakeWithJup, sendCompressedAirdrop, createOrcaSingleSidedWhirlpool, - FEE_TIERS, fetchPrice, + pythFetchPrice, + FEE_TIERS, } from "../tools"; import { CollectionDeployment, @@ -85,7 +86,7 @@ export class SolanaAgentKit { return deploy_collection(this, options); } - async getBalance(token_address?: PublicKey): Promise { + async getBalance(token_address?: PublicKey): Promise { return get_balance(this, token_address); } @@ -277,4 +278,8 @@ export class SolanaAgentKit { tickSize, ); } + + async pythFetchPrice(priceFeedID: string): Promise { + return pythFetchPrice(priceFeedID); + } } diff --git a/src/langchain/index.ts b/src/langchain/index.ts index c5db27c..72b9c05 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 { BN } from "@coral-xyz/anchor"; import { FEE_TIERS } from "../tools"; @@ -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), ]; } + diff --git a/src/tools/deploy_token.ts b/src/tools/deploy_token.ts index 5b33a12..7532eef 100644 --- a/src/tools/deploy_token.ts +++ b/src/tools/deploy_token.ts @@ -12,6 +12,7 @@ import { fromWeb3JsPublicKey, toWeb3JsPublicKey, } from "@metaplex-foundation/umi-web3js-adapters"; +import { mplToolbox } from "@metaplex-foundation/mpl-toolbox"; /** * Deploy a new SPL token @@ -33,7 +34,7 @@ export async function deploy_token( ): Promise<{ mint: PublicKey }> { try { // Create UMI instance from agent - const umi = createUmi(agent.connection.rpcEndpoint); + const umi = createUmi(agent.connection.rpcEndpoint).use(mplToolbox()); umi.use(keypairIdentity(fromWeb3JsKeypair(agent.wallet))); // Create new token mint diff --git a/src/tools/get_balance.ts b/src/tools/get_balance.ts index 36cdfef..05a56a1 100644 --- a/src/tools/get_balance.ts +++ b/src/tools/get_balance.ts @@ -10,7 +10,7 @@ import { SolanaAgentKit } from "../index"; export async function get_balance( agent: SolanaAgentKit, token_address?: PublicKey, -): Promise { +): Promise { if (!token_address) return ( (await agent.connection.getBalance(agent.wallet_address)) / @@ -19,5 +19,5 @@ export async function get_balance( const token_account = await agent.connection.getTokenAccountBalance(token_address); - return token_account.value.uiAmount; + return token_account.value.uiAmount || 0; } diff --git a/src/tools/index.ts b/src/tools/index.ts index 32a3c53..dcc93ef 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -20,3 +20,4 @@ export * from "./raydium_create_ammV4"; export * from "./raydium_create_clmm"; export * from "./raydium_create_cpmm"; export * from "./openbook_create_market"; +export * from "./pyth_fetch_price"; diff --git a/src/tools/pyth_fetch_price.ts b/src/tools/pyth_fetch_price.ts new file mode 100644 index 0000000..29129f8 --- /dev/null +++ b/src/tools/pyth_fetch_price.ts @@ -0,0 +1,40 @@ +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(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]; + + 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; +} diff --git a/test/index.ts b/test/index.ts index d0b72be..7cbf8bc 100644 --- a/test/index.ts +++ b/test/index.ts @@ -70,9 +70,8 @@ async function initializeAgent() { faucet. If not, you can provide your wallet details and request funds from the user. If there is a 5XX (internal) HTTP error code, ask the user to try again later. If someone asks you to do something you can't do with your currently available tools, you must say so, and encourage them to implement it - themselves using the Solana Agent Kit, recommend they go to https://solanaagentkit.xyz/ for more information. Be - concise and helpful with your responses. Refrain from restating your tools' descriptions unless it is - explicitly requested. + themselves using the Solana Agent Kit, recommend they go to https://www.solanaagentkit.xyz for more information. Be + concise and helpful with your responses. Refrain from restating your tools' descriptions unless it is explicitly requested. `, });