diff --git a/package.json b/package.json index 1880561..e041365 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@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", "@solana/web3.js": "^1.98.0", "@tiplink/api": "^0.3.1", "bn.js": "^5.2.1", diff --git a/src/agent/index.ts b/src/agent/index.ts index d2e10c0..c9958a2 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -43,6 +43,8 @@ import { orcaFetchPositions, rock_paper_scissor, create_TipLink, + listNFTForSale, + cancelListing, } from "../tools"; import { @@ -411,4 +413,15 @@ export class SolanaAgentKit { async createTiplink(amount: number, splmintAddress?: PublicKey) { return create_TipLink(this, amount, splmintAddress); } + + async tensorListNFT( + nftMint: PublicKey, + price: number, + ): Promise { + return listNFTForSale(this, nftMint, price); + } + + async tensorCancelListing(nftMint: PublicKey): Promise { + return cancelListing(this, nftMint); + } } diff --git a/src/langchain/index.ts b/src/langchain/index.ts index e17d25a..abd7f88 100644 --- a/src/langchain/index.ts +++ b/src/langchain/index.ts @@ -1590,6 +1590,96 @@ export class SolanaTipLinkTool extends Tool { } } +export class SolanaListNFTForSaleTool extends Tool { + name = "solana_list_nft_for_sale"; + description = `List an NFT for sale on Tensor Trade. + + Inputs (input is a JSON string): + nftMint: string, the mint address of the NFT (required) + price: number, price in SOL (required)`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const parsedInput = JSON.parse(input); + + // Validate NFT ownership first + const nftAccount = + await this.solanaKit.connection.getTokenAccountsByOwner( + this.solanaKit.wallet_address, + { mint: new PublicKey(parsedInput.nftMint) }, + ); + + if (nftAccount.value.length === 0) { + return JSON.stringify({ + status: "error", + message: + "NFT not found in wallet. Please make sure you own this NFT.", + code: "NFT_NOT_FOUND", + }); + } + + const tx = await this.solanaKit.tensorListNFT( + new PublicKey(parsedInput.nftMint), + parsedInput.price, + ); + + return JSON.stringify({ + status: "success", + message: "NFT listed for sale successfully", + transaction: tx, + price: parsedInput.price, + nftMint: parsedInput.nftMint, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "UNKNOWN_ERROR", + }); + } + } +} + + +export class SolanaCancelNFTListingTool extends Tool { + name = "solana_cancel_nft_listing"; + description = `Cancel an NFT listing on Tensor Trade. + + Inputs (input is a JSON string): + nftMint: string, the mint address of the NFT (required)`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const parsedInput = JSON.parse(input); + + const tx = await this.solanaKit.tensorCancelListing( + new PublicKey(parsedInput.nftMint), + ); + + return JSON.stringify({ + status: "success", + message: "NFT listing cancelled successfully", + transaction: tx, + nftMint: parsedInput.nftMint, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "UNKNOWN_ERROR", + }); + } + } +} + export function createSolanaTools(solanaKit: SolanaAgentKit) { return [ new SolanaBalanceTool(solanaKit), @@ -1632,5 +1722,7 @@ export function createSolanaTools(solanaKit: SolanaAgentKit) { new SolanaCreateGibworkTask(solanaKit), new SolanaRockPaperScissorsTool(solanaKit), new SolanaTipLinkTool(solanaKit), + new SolanaListNFTForSaleTool(solanaKit), + new SolanaCancelNFTListingTool(solanaKit), ]; } diff --git a/src/tools/index.ts b/src/tools/index.ts index af70276..c1584de 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -46,3 +46,5 @@ export * from "./create_gibwork_task"; export * from "./rock_paper_scissor"; export * from "./create_tiplinks"; + +export * from "./tensor_trade"; diff --git a/src/tools/tensor_trade.ts b/src/tools/tensor_trade.ts new file mode 100644 index 0000000..9be8a97 --- /dev/null +++ b/src/tools/tensor_trade.ts @@ -0,0 +1,108 @@ +import { SolanaAgentKit } from "../index"; +import { TensorSwapSDK } from "@tensor-oss/tensorswap-sdk"; +import { PublicKey, Transaction } from "@solana/web3.js"; +import { AnchorProvider, Wallet } from "@coral-xyz/anchor"; +import { BN } from "bn.js"; +import { + getAssociatedTokenAddress, + TOKEN_PROGRAM_ID, + getAccount, +} from "@solana/spl-token"; + +export async function listNFTForSale( + agent: SolanaAgentKit, + nftMint: PublicKey, + price: number, +): Promise { + try { + if (!PublicKey.isOnCurve(nftMint)) { + throw new Error("Invalid NFT mint address"); + } + + const mintInfo = await agent.connection.getAccountInfo(nftMint); + if (!mintInfo) { + throw new Error(`NFT mint ${nftMint.toString()} does not exist`); + } + + const ata = await getAssociatedTokenAddress(nftMint, agent.wallet_address); + + try { + const tokenAccount = await getAccount(agent.connection, ata); + + if (!tokenAccount || tokenAccount.amount <= 0) { + throw new Error(`You don't own this NFT (${nftMint.toString()})`); + } + } catch (e) { + throw new Error( + `No token account found for mint ${nftMint.toString()}. Make sure you own this NFT.`, + ); + } + + const provider = new AnchorProvider( + agent.connection, + new Wallet(agent.wallet), + AnchorProvider.defaultOptions(), + ); + + const tensorSwapSdk = new TensorSwapSDK({ provider }); + const priceInLamports = new BN(price * 1e9); + const nftSource = await getAssociatedTokenAddress( + nftMint, + agent.wallet_address, + ); + + const { tx } = await tensorSwapSdk.list({ + nftMint, + nftSource, + owner: agent.wallet_address, + price: priceInLamports, + tokenProgram: TOKEN_PROGRAM_ID, + payer: agent.wallet_address, + }); + + const transaction = new Transaction(); + transaction.add(...tx.ixs); + return await agent.connection.sendTransaction(transaction, [ + agent.wallet, + ...tx.extraSigners, + ]); + } catch (error: any) { + console.error("Full error details:", error); + throw error; + } +} + +export async function cancelListing( + agent: SolanaAgentKit, + nftMint: PublicKey, +): Promise { + const provider = new AnchorProvider( + agent.connection, + new Wallet(agent.wallet), + AnchorProvider.defaultOptions(), + ); + + const tensorSwapSdk = new TensorSwapSDK({ provider }); + const nftDest = await getAssociatedTokenAddress( + nftMint, + agent.wallet_address, + false, + TOKEN_PROGRAM_ID, + ); + + const { tx } = await tensorSwapSdk.delist({ + nftMint, + nftDest, + owner: agent.wallet_address, + tokenProgram: TOKEN_PROGRAM_ID, + payer: agent.wallet_address, + authData: null, + }); + + const transaction = new Transaction(); + transaction.add(...tx.ixs); + return await agent.connection.sendTransaction(transaction, [ + agent.wallet, + ...tx.extraSigners, + ]); +}