diff --git a/README.md b/README.md index a4a0093..7d49bc6 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,62 @@ 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 + +#### `deploy_token(agent, decimals?, name, uri, symbol, initialSupply?)` + +Deploy a new SPL token with optional initial supply. If not specified, decimals default to 9. + +#### `deploy_collection(agent, options)` + +Create a new NFT collection with customizable metadata and royalties. + +#### `mintCollectionNFT(agent, collectionMint, metadata, recipient?)` + +Mint a new NFT as part of an existing collection. + +#### `transfer(agent, to, amount, mint?)` + +Transfer SOL or SPL tokens to a recipient. + +#### `trade(agent, outputMint, inputAmount, inputMint?, slippageBps?)` + +Swap tokens using Jupiter Exchange integration. + +#### `get_balance(agent, token_address)` + +Check SOL or token balance for the agent's wallet. + +#### `lendAsset(agent, assetMint, amount, apiKey)` + +Lend idle assets to earn interest with Lulo. + +#### `stakeWithJup(agent, amount)` + +Stake SOL with Jupiter to earn rewards. + +#### `sendCompressedAirdrop(agent, mintAddress, amount, recipients, priorityFeeInLamports?, shouldLog?)` + +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 @@ -192,6 +248,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 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"; diff --git a/package.json b/package.json index 53165fb..790b80c 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,6 @@ "author": "", "license": "ISC", "dependencies": { - "@raydium-io/raydium-sdk-v2": "0.1.95-alpha", "@bonfida/spl-name-service": "^3.0.7", "@coral-xyz/anchor": "0.29", "@langchain/core": "^0.3.18", @@ -29,16 +28,19 @@ "@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", + "bn.js": "^5.2.1", "bs58": "^6.0.0", "decimal.js": "^10.4.3", - "bn.js": "^5.2.1", "dotenv": "^16.4.5", "form-data": "^4.0.1", "langchain": "^0.3.6", @@ -51,4 +53,4 @@ "ts-node": "^10.9.2", "typescript": "^5.7.2" } -} \ No newline at end of file +} 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 d8086b8..56cfe74 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -26,6 +26,7 @@ import { sendCompressedAirdrop, createOrcaSingleSidedWhirlpool, FEE_TIERS, + pythFetchPrice, } from "../tools"; import { CollectionDeployment, @@ -270,6 +271,10 @@ export class SolanaAgentKit { lotSize, 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), ]; } + diff --git a/src/tools/deploy_token.ts b/src/tools/deploy_token.ts index a36d5de..93502ec 100644 --- a/src/tools/deploy_token.ts +++ b/src/tools/deploy_token.ts @@ -4,6 +4,7 @@ import { createUmi } from "@metaplex-foundation/umi-bundle-defaults"; import { generateSigner, keypairIdentity } from "@metaplex-foundation/umi"; import { createFungible, mintV1, TokenStandard } from "@metaplex-foundation/mpl-token-metadata"; import { fromWeb3JsKeypair, fromWeb3JsPublicKey, toWeb3JsPublicKey } from "@metaplex-foundation/umi-web3js-adapters"; +import {mplToolbox} from "@metaplex-foundation/mpl-toolbox" /** * Deploy a new SPL token @@ -25,7 +26,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/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; +} diff --git a/test/index.ts b/test/index.ts index 9522c23..00faade 100644 --- a/test/index.ts +++ b/test/index.ts @@ -70,7 +70,7 @@ 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://sendai.fun/kit for more information. Be + 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. `,