diff --git a/src/langchain/index.ts b/src/langchain/index.ts index afb214f..ec1189e 100644 --- a/src/langchain/index.ts +++ b/src/langchain/index.ts @@ -751,15 +751,15 @@ export class SolanaCompressedAirdropTool extends Tool { export class SolanaCreateSingleSidedWhirlpoolTool extends Tool { name = "create_orca_single_sided_whirlpool"; - description = `Create a single-sided Whirlpool with liquidity. + description = `Create a single-sided liquidity pools with liquidity on Orca. Inputs (input is a JSON string): - - depositTokenAmount: number, eg: 1000000000 (required, in units of deposit token including decimals) - - depositTokenMint: string, eg: "DepositTokenMintAddress" (required, mint address of deposit token) - - otherTokenMint: string, eg: "OtherTokenMintAddress" (required, mint address of other token) - - initialPrice: number, eg: 0.001 (required, initial price of deposit token in terms of other token) - - maxPrice: number, eg: 5.0 (required, maximum price at which liquidity is added) - - feeTier: number, eg: 0.30 (required, fee tier for the pool)`; + - depositTokenAmount: number, in units of deposit token including decimals, eg: 1000000000 (required) + - depositTokenMint: string, mint address of deposit token, eg: "DepositTokenMintAddress" (required) + - otherTokenMint: string, mint address of other token, eg: "OtherTokenMintAddress" (required) + - initialPrice: number, initial price of deposit token in terms of other token, eg: 0.001, (required) + - maxPrice: number, maximum price at which liquidity is added, eg: 5.0 (required) + - feeTier: number, fee tier for the pool in %. Possible values on mainnet are: 0.01, 0.02, 0.04, 0.05, 0.16, 0.30, 0.65, 1.0, 2.0 (required)`; constructor(private solanaKit: SolanaAgentKit) { super(); @@ -803,7 +803,6 @@ export class SolanaCreateSingleSidedWhirlpoolTool extends Tool { } } - export class SolanaRaydiumCreateAmmV4 extends Tool { name = "raydium_create_ammV4"; description = `Raydium's Legacy AMM that requiers an OpenBook marketID diff --git a/src/tools/create_orca_single_sided_whirlpool.ts b/src/tools/create_orca_single_sided_whirlpool.ts index aefc93c..e0375f9 100644 --- a/src/tools/create_orca_single_sided_whirlpool.ts +++ b/src/tools/create_orca_single_sided_whirlpool.ts @@ -1,11 +1,10 @@ -import { Keypair, PublicKey, Transaction } from "@solana/web3.js"; +import { Keypair, PublicKey, Transaction, TransactionMessage, VersionedTransaction } from "@solana/web3.js"; import { SolanaAgentKit } from "../agent"; import { BN, Wallet } from "@coral-xyz/anchor"; import { Decimal } from "decimal.js"; import { PDAUtil, ORCA_WHIRLPOOL_PROGRAM_ID, - ORCA_WHIRLPOOLS_CONFIG, WhirlpoolContext, TickUtil, PriceMath, @@ -110,7 +109,7 @@ export const FEE_TIERS = { * const otherTokenMint = new PublicKey("OTHER_TOKEN_ADDRESS"); * const initialPrice = new Decimal(0.001); * const maxPrice = new Decimal(5.0); - * const feeTier = 0.30; + * const feeTier = 0.02; * * const txId = await createOrcaSingleSidedWhirlpool( * agent, @@ -133,6 +132,14 @@ export async function createOrcaSingleSidedWhirlpool( maxPrice: Decimal, feeTier: keyof typeof FEE_TIERS, ): Promise { + let whirlpoolsConfigAddress: PublicKey; + if (agent.connection.rpcEndpoint.includes('mainnet')) { + whirlpoolsConfigAddress = new PublicKey('2LecshUwdy9xi7meFgHtFJQNSKk4KdTrcpvaB56dP2NQ'); + } else if (agent.connection.rpcEndpoint.includes('devnet')) { + whirlpoolsConfigAddress = new PublicKey('FcrweFY1G9HJAHG5inkGB6pKg1HZ6x9UC2WioAfWrGkR'); + } else { + throw new Error('Unsupported network'); + } const wallet = new Wallet(agent.wallet); const ctx = WhirlpoolContext.from(agent.connection, wallet, ORCA_WHIRLPOOL_PROGRAM_ID); const fetcher = ctx.fetcher; @@ -163,7 +170,7 @@ export async function createOrcaSingleSidedWhirlpool( }; const feeTierKey = PDAUtil.getFeeTier( ORCA_WHIRLPOOL_PROGRAM_ID, - ORCA_WHIRLPOOLS_CONFIG, + whirlpoolsConfigAddress, tickSpacing, ).publicKey; const initSqrtPrice = PriceMath.tickIndexToSqrtPriceX64(initialTick); @@ -171,24 +178,24 @@ export async function createOrcaSingleSidedWhirlpool( const tokenVaultBKeypair = Keypair.generate(); const whirlpoolPda = PDAUtil.getWhirlpool( ORCA_WHIRLPOOL_PROGRAM_ID, - ORCA_WHIRLPOOLS_CONFIG, + whirlpoolsConfigAddress, mintA, mintB, FEE_TIERS[feeTier], ); const tokenBadgeA = PDAUtil.getTokenBadge( ORCA_WHIRLPOOL_PROGRAM_ID, - ORCA_WHIRLPOOLS_CONFIG, + whirlpoolsConfigAddress, mintA, ).publicKey; const tokenBadgeB = PDAUtil.getTokenBadge( ORCA_WHIRLPOOL_PROGRAM_ID, - ORCA_WHIRLPOOLS_CONFIG, + whirlpoolsConfigAddress, mintB, ).publicKey; const baseParamsPool = { initSqrtPrice, - whirlpoolsConfig: ORCA_WHIRLPOOLS_CONFIG, + whirlpoolsConfig: whirlpoolsConfigAddress, whirlpoolPda, tokenMintA: mintA, tokenMintB: mintB, @@ -301,7 +308,7 @@ export async function createOrcaSingleSidedWhirlpool( wallet.publicKey, undefined, ctx.accountResolverOpts.allowPDAOwnerAddress, - ctx.accountResolverOpts.createWrappedSolAccountMethod, + "ata", ); const { address: tokenOwnerAccountA, ...tokenOwnerAccountAIx } = ataA; const { address: tokenOwnerAccountB, ...tokenOwnerAccountBIx } = ataB; @@ -378,22 +385,18 @@ export async function createOrcaSingleSidedWhirlpool( }); txBuilder.addInstruction(liquidityIx); - const txPayload = await txBuilder.build({ - maxSupportedTransactionVersion: "legacy" - }); + const txPayload = await txBuilder.build(); + const instructions = TransactionMessage.decompile( + (txPayload.transaction as VersionedTransaction).message).instructions - if (txPayload.transaction instanceof Transaction) { - try { - const txId = await sendTx( - agent, - txPayload.transaction, - [positionMintKeypair, tokenVaultAKeypair, tokenVaultBKeypair], - ); - return txId; - } catch (error) { - throw new Error(`Failed to create pool: ${JSON.stringify(error)}`); - } - } else { - throw new Error('Failed to create pool: Transaction not created'); + try { + const txId = await sendTx( + agent, + instructions, + [positionMintKeypair, tokenVaultAKeypair, tokenVaultBKeypair], + ); + return txId; + } catch (error) { + throw new Error(`Failed to create pool: ${JSON.stringify(error)}`); } } diff --git a/src/utils/send_tx.ts b/src/utils/send_tx.ts index 903ddd5..7250c0c 100644 --- a/src/utils/send_tx.ts +++ b/src/utils/send_tx.ts @@ -1,68 +1,60 @@ import { SolanaAgentKit } from "../agent"; -import { Transaction, Keypair, TransactionInstruction } from "@solana/web3.js"; -import { Connection, ComputeBudgetProgram } from "@solana/web3.js"; +import { Keypair, TransactionInstruction, TransactionMessage, VersionedTransaction } from "@solana/web3.js"; +import { Connection, ComputeBudgetProgram, } from "@solana/web3.js"; + + +const feeTiers = { + min: 1, + mid: 0.5, + max: 0.95 +} /** * Get priority fees for the current block * @param connection - Solana RPC connection * @returns Priority fees statistics and instructions for different fee levels */ -export async function getPriorityFees(connection: Connection): Promise<{ - min: number; - median: number; - max: number; - instructions?: { - low: TransactionInstruction; - medium: TransactionInstruction; - high: TransactionInstruction; - }; -}> { +export async function getComputeBudgetInstructions(agent: SolanaAgentKit, instructions: TransactionInstruction[], feeTier: keyof typeof feeTiers): Promise<{ + blockhash: string; + computeBudgetLimitInstruction: TransactionInstruction; + computeBudgetPriorityFeeInstructions: TransactionInstruction; + } > { try { - // Get recent prioritization fees - const priorityFees = await connection.getRecentPrioritizationFees(); + const blockhash = (await agent.connection.getLatestBlockhash()).blockhash; + const messageV0 = new TransactionMessage({ + payerKey: agent.wallet_address, + recentBlockhash: blockhash, + instructions: instructions, + }).compileToV0Message(); + const transaction = new VersionedTransaction(messageV0); + const simulatedTx = agent.connection.simulateTransaction(transaction); + const estimatedComputeUnits = (await simulatedTx).value.unitsConsumed; + const safeComputeUnits = Math.ceil( + estimatedComputeUnits ? + Math.max(estimatedComputeUnits + 100000, estimatedComputeUnits * 1.2) + : 200000 + ); + const computeBudgetLimitInstruction = ComputeBudgetProgram.setComputeUnitLimit({ + units: safeComputeUnits, + }); - if (!priorityFees.length) { - return { - min: 0, - median: 0, - max: 0, - }; - } + const priorityFee = await agent.connection.getRecentPrioritizationFees() + .then(fees => + fees.sort((a, b) => a.prioritizationFee - b.prioritizationFee) + [Math.floor(fees.length * feeTiers[feeTier])].prioritizationFee + ); - // Sort fees by value - const sortedFees = priorityFees - .map((x) => x.prioritizationFee) - .sort((a, b) => a - b); - - // Calculate statistics - const min = sortedFees[0] ?? 0; - const max = sortedFees[sortedFees.length - 1] ?? 0; - const mid = Math.floor(sortedFees.length / 2); - const median = - sortedFees.length % 2 === 0 - ? ((sortedFees[mid - 1] ?? 0) + (sortedFees[mid] ?? 0)) / 2 - : sortedFees[mid] ?? 0; - - // Helper to create priority fee IX based on chosen strategy - const createPriorityFeeIx = (fee: number) => { - return ComputeBudgetProgram.setComputeUnitPrice({ - microLamports: fee, - }); - }; + const computeBudgetPriorityFeeInstructions = ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: priorityFee, + }); return { - min, - median, - max, - // Return instructions for different fee levels - instructions: { - low: createPriorityFeeIx(min), - medium: createPriorityFeeIx(median), - high: createPriorityFeeIx(max), - }, + blockhash, + computeBudgetLimitInstruction, + computeBudgetPriorityFeeInstructions }; } catch (error) { - console.error("Error getting priority fees:", error); + console.error("Error getting compute budget instructions fees:", error); throw error; } } @@ -75,24 +67,50 @@ export async function getPriorityFees(connection: Connection): Promise<{ */ export async function sendTx( agent: SolanaAgentKit, - tx: Transaction, + instructions: TransactionInstruction[], otherKeypairs?: Keypair[] ) { - tx.recentBlockhash = (await agent.connection.getLatestBlockhash()).blockhash; - tx.feePayer = agent.wallet_address; - const fees = await getPriorityFees(agent.connection); - if (fees.instructions) { - tx.add(fees.instructions.medium!); - } + console.log(instructions) - tx.sign(agent.wallet, ...(otherKeypairs ?? [])); - let txid = await agent.connection.sendRawTransaction(tx.serialize()); - await agent.connection.confirmTransaction({ - signature: txid, - blockhash: (await agent.connection.getLatestBlockhash()).blockhash, - lastValidBlockHeight: ( - await agent.connection.getLatestBlockhash() - ).lastValidBlockHeight, - }); - return txid; + const ixComputeBudget = await getComputeBudgetInstructions(agent, instructions, "mid"); + const allInstructions = [ + ixComputeBudget.computeBudgetLimitInstruction, + ixComputeBudget.computeBudgetPriorityFeeInstructions, + ...instructions]; + const messageV0 = new TransactionMessage({ + payerKey: agent.wallet_address, + recentBlockhash: ixComputeBudget.blockhash, + instructions: allInstructions, + }).compileToV0Message(); + const transaction = new VersionedTransaction(messageV0); + transaction.sign([agent.wallet, ...(otherKeypairs ?? [])]); + + const timeoutMs = 90000; + const startTime = Date.now(); + while (Date.now() - startTime < timeoutMs) { + const transactionStartTime = Date.now(); + + const signature = await agent.connection.sendTransaction( + transaction, + { + maxRetries: 0, + skipPreflight: true, + }); + + const statuses = await agent.connection.getSignatureStatuses([signature]); + if (statuses.value[0]) { + if (!statuses.value[0].err) { + return signature; + } else { + throw new Error(`Transaction failed: ${statuses.value[0].err.toString()}`); + } + } + + const elapsedTime = Date.now() - transactionStartTime; + const remainingTime = Math.max(0, 1000 - elapsedTime); + if (remainingTime > 0) { + await new Promise(resolve => setTimeout(resolve, remainingTime)); + } + } + throw new Error("Transaction timeout"); } diff --git a/test/index.ts b/test/index.ts index 00faade..bc16847 100644 --- a/test/index.ts +++ b/test/index.ts @@ -36,7 +36,7 @@ const WALLET_DATA_FILE = "wallet_data.txt"; async function initializeAgent() { try { const llm = new ChatOpenAI({ - modelName: "gpt-4o-mini", + modelName: "gpt-4o", temperature: 0.7, });