diff --git a/src/agent/index.ts b/src/agent/index.ts index b39eaa7..da2e489 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -27,6 +27,7 @@ import { orcaCreateSingleSidedLiquidityPool, orcaCreateCLMM, orcaOpenCenteredPositionWithLiquidity, + orcaOpenSingleSidedPosition, FEE_TIERS, fetchPrice, pythFetchPrice, @@ -37,6 +38,8 @@ import { getOwnedAllDomains, resolveAllDomains, create_gibwork_task, + orcaClosePosition, + orcaFetchPositions, } from "../tools"; import { @@ -202,23 +205,32 @@ export class SolanaAgentKit { ); } + async orcaClosePosition( + positionMintAddress: PublicKey, + ) { + return orcaClosePosition( + this, + positionMintAddress, + ); + } + async orcaCreateCLMM( - mintA: PublicKey, - mintB: PublicKey, + mintDeploy: PublicKey, + mintPair: PublicKey, initialPrice: Decimal, feeTier: keyof typeof FEE_TIERS, ) { return orcaCreateCLMM( this, - mintA, - mintB, + mintDeploy, + mintPair, initialPrice, feeTier, ); } async orcaCreateSingleSidedLiquidityPool( - depositTokenAmount: BN, + depositTokenAmount: number, depositTokenMint: PublicKey, otherTokenMint: PublicKey, initialPrice: Decimal, @@ -236,6 +248,13 @@ export class SolanaAgentKit { ); } + async orcaFetchPositions( + ) { + return orcaFetchPositions( + this, + ); + } + async orcaOpenCenteredPositionWithLiquidity( whirlpoolAddress: PublicKey, priceOffsetBps: number, @@ -251,6 +270,23 @@ export class SolanaAgentKit { ); } + async orcaOpenSingleSidedPosition( + whirlpoolAddress: PublicKey, + distanceFromCurrentPriceBps: number, + widthBps: number, + inputTokenMint: PublicKey, + inputAmount: Decimal, + ): Promise { + return orcaOpenSingleSidedPosition( + this, + whirlpoolAddress, + distanceFromCurrentPriceBps, + widthBps, + inputTokenMint, + inputAmount, + ); + } + async resolveAllDomains(domain: string): Promise { return resolveAllDomains(this, domain); } diff --git a/src/langchain/index.ts b/src/langchain/index.ts index 21da295..d627984 100644 --- a/src/langchain/index.ts +++ b/src/langchain/index.ts @@ -10,6 +10,7 @@ import { create_image } from "../tools/create_image"; import { BN } from "@coral-xyz/anchor"; import { FEE_TIERS } from "../tools"; import { toJSON } from "../utils/toJSON"; +import { s } from "@raydium-io/raydium-sdk-v2/lib/api-0eb57ba2"; export class SolanaBalanceTool extends Tool { name = "solana_balance"; @@ -754,13 +755,49 @@ export class SolanaCompressedAirdropTool extends Tool { } } +export class SolanaClosePostition extends Tool { + name = "orca_close_position"; + description = `Closes an existing liquidity position in an Orca Whirlpool. This function fetches the position + details using the provided mint address and closes the position with a 1% slippage. + + Inputs (JSON string): + - positionMintAddress: string, the address of the position mint that represents the liquidity position.` + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + async _call(input: string): Promise { + try { + const inputFormat = JSON.parse(input); + const positionMintAddress = new PublicKey(inputFormat.positionMintAddress); + + const txId = await this.solanaKit.orcaClosePosition( + positionMintAddress, + ); + + return JSON.stringify({ + status: "success", + message: "Liquidity position closed successfully.", + transaction: txId, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "UNKNOWN_ERROR", + }); + } + } +} + export class SolanaOrcaCreateCLMM extends Tool { name = "orca_create_clmm"; - description = `Create a Concentrated Liquidity Market Maker (CLMM) pool on Orca, the most efficient and capital-optimized CLMM on Solana. This function initializes a CLMM pool but does not add liquidity. You can add liquidity later using a centered position or a single-sided position. Single-sided positions are ideal for directional price expectations, acting as sort of a limit call. Centered positions are ideal for generating yield. The tighter, the better. + description = `Create a Concentrated Liquidity Market Maker (CLMM) pool on Orca, the most efficient and capital-optimized CLMM on Solana. This function initializes a CLMM pool but does not add liquidity. You can add liquidity later using a centered position or a single-sided position. Inputs (JSON string): - - mintA: string, mint address of the first token, e.g., "MintAAddress" (required). - - mintB: string, mint address of the second token, e.g., "MintBAddress" (required). + - mintDeploy: string, the mint of the token you want to deploy (required). + - mintPair: string, The mint of the token you want to pair the deployed mint with (required). - initialPrice: number, initial price of mintA in terms of mintB, e.g., 0.001 (required). - feeTier: number, fee tier in bps. Options: 1, 2, 4, 5, 16, 30, 65, 100, 200 (required).`; @@ -771,8 +808,8 @@ export class SolanaOrcaCreateCLMM extends Tool { async _call(input: string): Promise { try { const inputFormat = JSON.parse(input); - const mintA = new PublicKey(inputFormat.mintA); - const mintB = new PublicKey(inputFormat.mintB); + const mintA = new PublicKey(inputFormat.mintDeploy); + const mintB = new PublicKey(inputFormat.mintPair); const initialPrice = new Decimal(inputFormat.initialPrice); const feeTier = inputFormat.feeTier; @@ -828,7 +865,7 @@ export class SolanaOrcaCreateSingleSideLiquidityPool extends Tool { async _call(input: string): Promise { try { const inputFormat = JSON.parse(input); - const depositTokenAmount = new BN(inputFormat.depositTokenAmount); + const depositTokenAmount = inputFormat.depositTokenAmount; const depositTokenMint = new PublicKey(inputFormat.depositTokenMint); const otherTokenMint = new PublicKey(inputFormat.otherTokenMint); const initialPrice = new Decimal(inputFormat.initialPrice); @@ -867,9 +904,37 @@ export class SolanaOrcaCreateSingleSideLiquidityPool extends Tool { } } +export class SolanaOrcaFetchPositions extends Tool { + name = "orca_fetch_positions"; + description = `Fetch all the liquidity positions in an Orca Whirlpool by owner. Returns an object with positiont mint addresses as keys and position status details as values.`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + async _call(): Promise { + try { + + const txId = await this.solanaKit.orcaFetchPositions() + + return JSON.stringify({ + status: "success", + message: "Liquidity positions fetched.", + transaction: txId, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "UNKNOWN_ERROR", + }); + } + } +} + export class SolanaOrcaOpenCenteredPosition extends Tool { name = "orca_open_centered_position_with_liquidity"; - description = `Add liquidity to a CLMM by opening a position in an Orca Whirlpool, the most efficient CLMM on Solana for precise liquidity management. A tighter range concentrates more liquidity, increasing yield potential. This function calculates the required amount of the other token based on the input token and desired offset. Centered positions are ideal for optimizing returns within defined ranges. + description = `Add liquidity to a CLMM by opening a centered position in an Orca Whirlpool, the most efficient liquidity pool on Solana. Inputs (JSON string): - whirlpoolAddress: string, address of the Orca Whirlpool (required). @@ -889,8 +954,8 @@ export class SolanaOrcaOpenCenteredPosition extends Tool { const inputTokenMint = new PublicKey(inputFormat.inputTokenMint); const inputAmount = new Decimal(inputFormat.inputAmount); - if (priceOffsetBps <= 0 || priceOffsetBps > 10_000) { - throw new Error("Invalid priceOffsetBps. It must be greater than 0 and less than or equal to 10,000."); + if (priceOffsetBps < 0 ) { + throw new Error("Invalid distanceFromCurrentPriceBps. It must be equal or greater than 0."); } const txId = await this.solanaKit.orcaOpenCenteredPositionWithLiquidity( @@ -915,6 +980,57 @@ export class SolanaOrcaOpenCenteredPosition extends Tool { } } +export class SolanaOrcaOpenSingleSidedPosition extends Tool { + name = "orca_open_single_sided_position"; + description = `Add liquidity to a CLMM by opening a single-sided position in an Orca Whirlpool, the most efficient liquidity pool on Solana. + + Inputs (JSON string): + - whirlpoolAddress: string, address of the Orca Whirlpool (required). + - distanceFromCurrentPriceBps: number, distance in basis points from the current price for the position (required). + - widthBps: number, width of the position in basis points (required). + - inputTokenMint: string, mint address of the deposit token (required). + - inputAmount: number, amount of the deposit token, e.g., 100.0 (required).`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + async _call(input: string): Promise { + try { + const inputFormat = JSON.parse(input); + const whirlpoolAddress = new PublicKey(inputFormat.whirlpoolAddress); + const distanceFromCurrentPriceBps = inputFormat.distanceFromCurrentPriceBps; + const widthBps = inputFormat.widthBps; + const inputTokenMint = new PublicKey(inputFormat.inputTokenMint); + const inputAmount = new Decimal(inputFormat.inputAmount); + + if (distanceFromCurrentPriceBps < 0 || widthBps < 0) { + throw new Error("Invalid distanceFromCurrentPriceBps or width. It must be equal or greater than 0."); + } + + const txId = await this.solanaKit.orcaOpenSingleSidedPosition( + whirlpoolAddress, + distanceFromCurrentPriceBps, + widthBps, + inputTokenMint, + inputAmount, + ); + + return JSON.stringify({ + status: "success", + message: "Single-sided liquidity position opened successfully.", + transaction: txId, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "UNKNOWN_ERROR", + }); + } + } +} + export class SolanaRaydiumCreateAmmV4 extends Tool { name = "raydium_create_ammV4"; description = `Raydium's Legacy AMM that requiers an OpenBook marketID @@ -1357,9 +1473,12 @@ export function createSolanaTools(solanaKit: SolanaAgentKit) { new SolanaRaydiumCreateClmm(solanaKit), new SolanaRaydiumCreateCpmm(solanaKit), new SolanaOpenbookCreateMarket(solanaKit), + new SolanaClosePostition(solanaKit), new SolanaOrcaCreateCLMM(solanaKit), new SolanaOrcaCreateSingleSideLiquidityPool(solanaKit), + new SolanaOrcaFetchPositions(solanaKit), new SolanaOrcaOpenCenteredPosition(solanaKit), + new SolanaOrcaOpenSingleSidedPosition(solanaKit), new SolanaPythFetchPrice(solanaKit), new SolanaResolveDomainTool(solanaKit), new SolanaGetOwnedDomains(solanaKit), diff --git a/src/tools/deploy_token.ts b/src/tools/deploy_token.ts index 7532eef..798e020 100644 --- a/src/tools/deploy_token.ts +++ b/src/tools/deploy_token.ts @@ -59,7 +59,7 @@ export async function deploy_token( mint: mint.publicKey, tokenStandard: TokenStandard.Fungible, tokenOwner: fromWeb3JsPublicKey(agent.wallet_address), - amount: initialSupply, + amount: initialSupply * Math.pow(10, decimals), }), ); } diff --git a/src/tools/index.ts b/src/tools/index.ts index a87f931..863a68a 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -15,9 +15,12 @@ export * from "./get_token_data"; export * from "./stake_with_jup"; export * from "./fetch_price"; export * from "./send_compressed_airdrop"; +export * from "./orca_close_position" export * from "./orca_create_clmm"; export * from "./orca_create_single_sided_liquidity_pool"; +export * from "./orca_fetch_positions"; export * from "./orca_open_centered_position_with_liquidity"; +export * from "./orca_open_single_sided_position"; export * from "./get_all_domains_tlds"; export * from "./get_all_registered_all_domains"; export * from "./get_owned_domains_for_tld"; diff --git a/src/tools/orca_close_position.ts b/src/tools/orca_close_position.ts new file mode 100644 index 0000000..976d6fc --- /dev/null +++ b/src/tools/orca_close_position.ts @@ -0,0 +1,78 @@ +import { + Keypair, + PublicKey, + TransactionMessage, + VersionedTransaction +} from "@solana/web3.js"; +import { SolanaAgentKit } from "../agent"; +import { Wallet } from "@coral-xyz/anchor"; +import { + ORCA_WHIRLPOOL_PROGRAM_ID, + WhirlpoolContext, + buildWhirlpoolClient, + PDAUtil, +} from "@orca-so/whirlpools-sdk"; +import { sendTx } from "../utils/send_tx"; +import { Percentage } from "@orca-so/common-sdk"; + +/** + * # Closes a Liquidity Position in an Orca Whirlpool + * + * This function closes an existing liquidity position in a specified Orca Whirlpool. The user provides + * the position's mint address. + * + * ## Parameters + * - `agent`: The `SolanaAgentKit` instance representing the wallet and connection details. + * - `positionMintAddress`: The mint address of the liquidity position to close. + * + * ## Returns + * A `Promise` that resolves to a `string` containing the transaction ID of the transaction + * + * ## Notes + * - The function uses Orca’s SDK to interact with the specified Whirlpool and close the liquidity position. + * - A maximum slippage of 1% is assumed for liquidity provision during the position closing. + * - The function automatically fetches the associated Whirlpool address and position details using the provided mint address. + * + * ## Throws + * An error will be thrown if: + * - The specified position mint address is invalid or inaccessible. + * - The transaction fails to send. + * - Any required position or Whirlpool data cannot be fetched. + * + * @param agent - The `SolanaAgentKit` instance representing the wallet and connection. + * @param positionMintAddress - The mint address of the liquidity position to close. + * @returns A promise resolving to the transaction ID (`string`). + */ +export async function orcaClosePosition( + agent: SolanaAgentKit, + positionMintAddress: PublicKey, +): Promise { + try { + const wallet = new Wallet(agent.wallet); + const ctx = WhirlpoolContext.from( + agent.connection, + wallet, + ORCA_WHIRLPOOL_PROGRAM_ID, + ); + const client = buildWhirlpoolClient(ctx) + + const positionAddress = PDAUtil.getPosition(ORCA_WHIRLPOOL_PROGRAM_ID, positionMintAddress); + const position = await client.getPosition(positionAddress.publicKey); + const whirlpoolAddress = position.getData().whirlpool; + const whirlpool = await client.getPool(whirlpoolAddress); + const txBuilder = await whirlpool.closePosition(positionAddress.publicKey, Percentage.fromFraction(1, 100)); + const txPayload = await txBuilder[0].build(); + const txPayloadDecompiled = TransactionMessage.decompile((txPayload.transaction as VersionedTransaction).message); + const instructions = txPayloadDecompiled.instructions; + const signers = txPayload.signers as Keypair[]; + + const txId = await sendTx( + agent, + instructions, + signers + ); + return txId + } catch (error) { + throw new Error(`${error}`); + } +} \ No newline at end of file diff --git a/src/tools/orca_create_clmm.ts b/src/tools/orca_create_clmm.ts index 4b6d465..5c6b402 100644 --- a/src/tools/orca_create_clmm.ts +++ b/src/tools/orca_create_clmm.ts @@ -1,4 +1,9 @@ -import { Keypair, PublicKey, TransactionMessage, VersionedTransaction } from "@solana/web3.js"; +import { + Keypair, + PublicKey, + TransactionMessage, + VersionedTransaction +} from "@solana/web3.js"; import { SolanaAgentKit } from "../agent"; import { Wallet } from "@coral-xyz/anchor"; import { Decimal } from "decimal.js"; @@ -9,7 +14,6 @@ import { PoolUtil, buildWhirlpoolClient, } from "@orca-so/whirlpools-sdk"; - import { sendTx } from "../utils/send_tx"; import { FEE_TIERS } from "./orca_create_single_sided_liquidity_pool"; @@ -28,9 +32,9 @@ import { FEE_TIERS } from "./orca_create_single_sided_liquidity_pool"; * adjusts the input order as needed and inverts the initial price accordingly. * * @param agent - The `SolanaAgentKit` instance representing the wallet and connection details. - * @param mintA - The mint address of the first token in the pool (e.g., SHARK). - * @param mintB - The mint address of the second token in the pool (e.g., USDC). - * @param initialPrice - The initial price of `mintA` in terms of `mintB`. + * @param mintDeploy - The mint of the token you want to deploy (e.g., SHARK). + * @param mintPair - The mint of the token you want to pair the deployed mint with (e.g., USDC). + * @param initialPrice - The initial price of `mintDeploy` in terms of `mintPair`. * @param feeTier - The fee tier bps for the pool, determining tick spacing and fee collection rates. * * @returns A promise that resolves to a transaction ID (`string`) of the transaction creating the pool. @@ -46,8 +50,8 @@ import { FEE_TIERS } from "./orca_create_single_sided_liquidity_pool"; */ export async function orcaCreateCLMM( agent: SolanaAgentKit, - mintA: PublicKey, - mintB: PublicKey, + mintDeploy: PublicKey, + mintPair: PublicKey, initialPrice: Decimal, feeTier: keyof typeof FEE_TIERS, ): Promise { @@ -70,14 +74,18 @@ export async function orcaCreateCLMM( const client = buildWhirlpoolClient(ctx) const correctTokenOrder = PoolUtil.orderMints( - mintA, - mintB, + mintDeploy, + mintPair, ).map((addr) => addr.toString()); const isCorrectMintOrder = - correctTokenOrder[0] === mintA.toString(); + correctTokenOrder[0] === mintDeploy.toString(); + let mintA; + let mintB; if (!isCorrectMintOrder) { - [mintA, mintB] = [mintB, mintA]; + [mintA, mintB] = [mintPair, mintDeploy]; initialPrice = new Decimal(1 / initialPrice.toNumber()); + } else { + [mintA, mintB] = [mintDeploy, mintPair]; } const mintAAccount = await fetcher.getMintInfo(mintA); const mintBAccount = await fetcher.getMintInfo(mintB); @@ -85,9 +93,8 @@ export async function orcaCreateCLMM( throw Error("Mint account not found"); } - const initialTick = PriceMath.priceToTickIndex(initialPrice, mintAAccount.decimals, mintBAccount.decimals) const tickSpacing = FEE_TIERS[feeTier]; - + const initialTick = PriceMath.priceToInitializableTickIndex(initialPrice, mintAAccount.decimals, mintBAccount.decimals, tickSpacing) const { poolKey, tx: txBuilder } = await client.createPool( whirlpoolsConfigAddress, mintA, diff --git a/src/tools/orca_create_single_sided_liquidity_pool.ts b/src/tools/orca_create_single_sided_liquidity_pool.ts index 932f119..68602b9 100644 --- a/src/tools/orca_create_single_sided_liquidity_pool.ts +++ b/src/tools/orca_create_single_sided_liquidity_pool.ts @@ -84,7 +84,7 @@ export const FEE_TIERS = { * be 1/`initialPrice`. This will not affect the price of the token as you intended it to be. * * @param agent - The `SolanaAgentKit` instance representing the wallet and connection details. - * @param depositTokenAmount - The amount of the deposit token (including the decimals) to contribute to the pool. + * @param depositTokenAmount - The amount of the deposit token to deposit in the pool. * @param depositTokenMint - The mint address of the token being deposited into the pool, eg. SHARK. * @param otherTokenMint - The mint address of the other token in the pool, eg. USDC. * @param initialPrice - The initial price of the deposit token in terms of the other token. @@ -103,304 +103,308 @@ export const FEE_TIERS = { */ export async function orcaCreateSingleSidedLiquidityPool( agent: SolanaAgentKit, - depositTokenAmount: BN, + depositTokenAmount: number, depositTokenMint: PublicKey, otherTokenMint: PublicKey, initialPrice: Decimal, maxPrice: Decimal, feeTierBps: 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; - - const correctTokenOrder = PoolUtil.orderMints( - otherTokenMint, - depositTokenMint, - ).map((addr) => addr.toString()); - const isCorrectMintOrder = - correctTokenOrder[0] === depositTokenMint.toString(); - let mintA, mintB; - if (isCorrectMintOrder) { - [mintA, mintB] = [depositTokenMint, otherTokenMint]; - } else { - [mintA, mintB] = [otherTokenMint, depositTokenMint]; - initialPrice = new Decimal(1 / initialPrice.toNumber()); - maxPrice = new Decimal(1 / maxPrice.toNumber()); - } - const mintAAccount = await fetcher.getMintInfo(mintA); - const mintBAccount = await fetcher.getMintInfo(mintB); - if (mintAAccount === null || mintBAccount === null) { - throw Error("Mint account not found"); - } - const tickSpacing = FEE_TIERS[feeTierBps]; - const tickIndex = PriceMath.priceToTickIndex( - initialPrice, - mintAAccount.decimals, - mintBAccount.decimals, - ); - const initialTick = TickUtil.getInitializableTickIndex( - tickIndex, - tickSpacing, - ); - - const tokenExtensionCtx: TokenExtensionContextForPool = { - ...NO_TOKEN_EXTENSION_CONTEXT, - tokenMintWithProgramA: mintAAccount, - tokenMintWithProgramB: mintBAccount, - }; - const feeTierKey = PDAUtil.getFeeTier( - ORCA_WHIRLPOOL_PROGRAM_ID, - whirlpoolsConfigAddress, - tickSpacing, - ).publicKey; - const initSqrtPrice = PriceMath.tickIndexToSqrtPriceX64(initialTick); - const tokenVaultAKeypair = Keypair.generate(); - const tokenVaultBKeypair = Keypair.generate(); - const whirlpoolPda = PDAUtil.getWhirlpool( - ORCA_WHIRLPOOL_PROGRAM_ID, - whirlpoolsConfigAddress, - mintA, - mintB, - FEE_TIERS[feeTierBps], - ); - const tokenBadgeA = PDAUtil.getTokenBadge( - ORCA_WHIRLPOOL_PROGRAM_ID, - whirlpoolsConfigAddress, - mintA, - ).publicKey; - const tokenBadgeB = PDAUtil.getTokenBadge( - ORCA_WHIRLPOOL_PROGRAM_ID, - whirlpoolsConfigAddress, - mintB, - ).publicKey; - const baseParamsPool = { - initSqrtPrice, - whirlpoolsConfig: whirlpoolsConfigAddress, - whirlpoolPda, - tokenMintA: mintA, - tokenMintB: mintB, - tokenVaultAKeypair, - tokenVaultBKeypair, - feeTierKey, - tickSpacing: tickSpacing, - funder: wallet.publicKey, - }; - const initPoolIx = !TokenExtensionUtil.isV2IxRequiredPool(tokenExtensionCtx) - ? WhirlpoolIx.initializePoolIx(ctx.program, baseParamsPool) - : WhirlpoolIx.initializePoolV2Ix(ctx.program, { - ...baseParamsPool, - tokenProgramA: tokenExtensionCtx.tokenMintWithProgramA.tokenProgram, - tokenProgramB: tokenExtensionCtx.tokenMintWithProgramB.tokenProgram, - tokenBadgeA, - tokenBadgeB, - }); - const initialTickArrayStartTick = TickUtil.getStartTickIndex( - initialTick, - tickSpacing, - ); - const initialTickArrayPda = PDAUtil.getTickArray( - ctx.program.programId, - whirlpoolPda.publicKey, - initialTickArrayStartTick, - ); - - const txBuilder = new TransactionBuilder( - ctx.provider.connection, - ctx.provider.wallet, - ctx.txBuilderOpts, - ); - txBuilder.addInstruction(initPoolIx); - txBuilder.addInstruction( - initTickArrayIx(ctx.program, { - startTick: initialTickArrayStartTick, - tickArrayPda: initialTickArrayPda, - whirlpool: whirlpoolPda.publicKey, - funder: wallet.publicKey, - }), - ); - - let tickLowerIndex, tickUpperIndex; - if (isCorrectMintOrder) { - tickLowerIndex = initialTick; - tickUpperIndex = PriceMath.priceToTickIndex( - maxPrice, - mintAAccount.decimals, - mintBAccount.decimals, + try { + 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, ); - } else { - tickLowerIndex = PriceMath.priceToTickIndex( - maxPrice, - mintAAccount.decimals, - mintBAccount.decimals, - ); - tickUpperIndex = initialTick; - } - const tickLowerInitializableIndex = TickUtil.getInitializableTickIndex( - tickLowerIndex, - tickSpacing, - ); - const tickUpperInitializableIndex = TickUtil.getInitializableTickIndex( - tickUpperIndex, - tickSpacing, - ); - if ( - !TickUtil.checkTickInBounds(tickLowerInitializableIndex) || - !TickUtil.checkTickInBounds(tickUpperInitializableIndex) - ) { - throw Error("Prices out of bounds"); - } - const increasLiquidityQuoteParam: IncreaseLiquidityQuoteParam = { - inputTokenAmount: new BN(depositTokenAmount), - inputTokenMint: depositTokenMint, - tokenMintA: mintA, - tokenMintB: mintB, - tickCurrentIndex: initialTick, - sqrtPrice: initSqrtPrice, - tickLowerIndex: tickLowerInitializableIndex, - tickUpperIndex: tickUpperInitializableIndex, - tokenExtensionCtx: tokenExtensionCtx, - slippageTolerance: Percentage.fromFraction(0, 100), - }; - const liquidityInput = increaseLiquidityQuoteByInputTokenWithParams( - increasLiquidityQuoteParam, - ); - const { liquidityAmount: liquidity, tokenMaxA, tokenMaxB } = liquidityInput; + const fetcher = ctx.fetcher; - const positionMintKeypair = Keypair.generate(); - const positionMintPubkey = positionMintKeypair.publicKey; - const positionPda = PDAUtil.getPosition( - ORCA_WHIRLPOOL_PROGRAM_ID, - positionMintPubkey, - ); - const positionTokenAccountAddress = getAssociatedTokenAddressSync( - positionMintPubkey, - wallet.publicKey, - ctx.accountResolverOpts.allowPDAOwnerAddress, - TOKEN_2022_PROGRAM_ID, - ); - const params = { - funder: wallet.publicKey, - owner: wallet.publicKey, - positionPda, - positionTokenAccount: positionTokenAccountAddress, - whirlpool: whirlpoolPda.publicKey, - tickLowerIndex: tickLowerInitializableIndex, - tickUpperIndex: tickUpperInitializableIndex, - }; - const positionIx = openPositionWithTokenExtensionsIx(ctx.program, { - ...params, - positionMint: positionMintPubkey, - withTokenMetadataExtension: true, - }); - - txBuilder.addInstruction(positionIx); - txBuilder.addSigner(positionMintKeypair); - - const [ataA, ataB] = await resolveOrCreateATAs( - ctx.connection, - wallet.publicKey, - [ - { tokenMint: mintA, wrappedSolAmountIn: tokenMaxA }, - { tokenMint: mintB, wrappedSolAmountIn: tokenMaxB }, - ], - () => ctx.fetcher.getAccountRentExempt(), - wallet.publicKey, - undefined, - ctx.accountResolverOpts.allowPDAOwnerAddress, - "ata", - ); - const { address: tokenOwnerAccountA, ...tokenOwnerAccountAIx } = ataA; - const { address: tokenOwnerAccountB, ...tokenOwnerAccountBIx } = ataB; - - txBuilder.addInstruction(tokenOwnerAccountAIx); - txBuilder.addInstruction(tokenOwnerAccountBIx); - - const tickArrayLowerStartIndex = TickUtil.getStartTickIndex( - tickLowerInitializableIndex, - tickSpacing, - ); - const tickArrayUpperStartIndex = TickUtil.getStartTickIndex( - tickUpperInitializableIndex, - tickSpacing, - ); - const tickArrayLowerPda = PDAUtil.getTickArray( - ctx.program.programId, - whirlpoolPda.publicKey, - tickArrayLowerStartIndex, - ); - const tickArrayUpperPda = PDAUtil.getTickArray( - ctx.program.programId, - whirlpoolPda.publicKey, - tickArrayUpperStartIndex, - ); - if (tickArrayUpperStartIndex !== tickArrayLowerStartIndex) { + const correctTokenOrder = PoolUtil.orderMints( + otherTokenMint, + depositTokenMint, + ).map((addr) => addr.toString()); + const isCorrectMintOrder = + correctTokenOrder[0] === depositTokenMint.toString(); + let mintA, mintB; if (isCorrectMintOrder) { - txBuilder.addInstruction( - initTickArrayIx(ctx.program, { - startTick: tickArrayUpperStartIndex, - tickArrayPda: tickArrayUpperPda, - whirlpool: whirlpoolPda.publicKey, - funder: wallet.publicKey, - }), + [mintA, mintB] = [depositTokenMint, otherTokenMint]; + } else { + [mintA, mintB] = [otherTokenMint, depositTokenMint]; + initialPrice = new Decimal(1 / initialPrice.toNumber()); + maxPrice = new Decimal(1 / maxPrice.toNumber()); + } + const mintAAccount = await fetcher.getMintInfo(mintA); + const mintBAccount = await fetcher.getMintInfo(mintB); + if (mintAAccount === null || mintBAccount === null) { + throw Error("Mint account not found"); + } + const tickSpacing = FEE_TIERS[feeTierBps]; + const tickIndex = PriceMath.priceToTickIndex( + initialPrice, + mintAAccount.decimals, + mintBAccount.decimals, + ); + const initialTick = TickUtil.getInitializableTickIndex( + tickIndex, + tickSpacing, + ); + + const tokenExtensionCtx: TokenExtensionContextForPool = { + ...NO_TOKEN_EXTENSION_CONTEXT, + tokenMintWithProgramA: mintAAccount, + tokenMintWithProgramB: mintBAccount, + }; + const feeTierKey = PDAUtil.getFeeTier( + ORCA_WHIRLPOOL_PROGRAM_ID, + whirlpoolsConfigAddress, + tickSpacing, + ).publicKey; + const initSqrtPrice = PriceMath.tickIndexToSqrtPriceX64(initialTick); + const tokenVaultAKeypair = Keypair.generate(); + const tokenVaultBKeypair = Keypair.generate(); + const whirlpoolPda = PDAUtil.getWhirlpool( + ORCA_WHIRLPOOL_PROGRAM_ID, + whirlpoolsConfigAddress, + mintA, + mintB, + FEE_TIERS[feeTierBps], + ); + const tokenBadgeA = PDAUtil.getTokenBadge( + ORCA_WHIRLPOOL_PROGRAM_ID, + whirlpoolsConfigAddress, + mintA, + ).publicKey; + const tokenBadgeB = PDAUtil.getTokenBadge( + ORCA_WHIRLPOOL_PROGRAM_ID, + whirlpoolsConfigAddress, + mintB, + ).publicKey; + const baseParamsPool = { + initSqrtPrice, + whirlpoolsConfig: whirlpoolsConfigAddress, + whirlpoolPda, + tokenMintA: mintA, + tokenMintB: mintB, + tokenVaultAKeypair, + tokenVaultBKeypair, + feeTierKey, + tickSpacing: tickSpacing, + funder: wallet.publicKey, + }; + const initPoolIx = !TokenExtensionUtil.isV2IxRequiredPool(tokenExtensionCtx) + ? WhirlpoolIx.initializePoolIx(ctx.program, baseParamsPool) + : WhirlpoolIx.initializePoolV2Ix(ctx.program, { + ...baseParamsPool, + tokenProgramA: tokenExtensionCtx.tokenMintWithProgramA.tokenProgram, + tokenProgramB: tokenExtensionCtx.tokenMintWithProgramB.tokenProgram, + tokenBadgeA, + tokenBadgeB, + }); + const initialTickArrayStartTick = TickUtil.getStartTickIndex( + initialTick, + tickSpacing, + ); + const initialTickArrayPda = PDAUtil.getTickArray( + ctx.program.programId, + whirlpoolPda.publicKey, + initialTickArrayStartTick, + ); + + const txBuilder = new TransactionBuilder( + ctx.provider.connection, + ctx.provider.wallet, + ctx.txBuilderOpts, + ); + txBuilder.addInstruction(initPoolIx); + txBuilder.addInstruction( + initTickArrayIx(ctx.program, { + startTick: initialTickArrayStartTick, + tickArrayPda: initialTickArrayPda, + whirlpool: whirlpoolPda.publicKey, + funder: wallet.publicKey, + }), + ); + + let tickLowerIndex, tickUpperIndex; + if (isCorrectMintOrder) { + tickLowerIndex = initialTick; + tickUpperIndex = PriceMath.priceToTickIndex( + maxPrice, + mintAAccount.decimals, + mintBAccount.decimals, ); } else { - txBuilder.addInstruction( - initTickArrayIx(ctx.program, { - startTick: tickArrayLowerStartIndex, - tickArrayPda: tickArrayLowerPda, - whirlpool: whirlpoolPda.publicKey, - funder: wallet.publicKey, - }), + tickLowerIndex = PriceMath.priceToTickIndex( + maxPrice, + mintAAccount.decimals, + mintBAccount.decimals, ); + tickUpperIndex = initialTick; } - } + const tickLowerInitializableIndex = TickUtil.getInitializableTickIndex( + tickLowerIndex, + tickSpacing, + ); + const tickUpperInitializableIndex = TickUtil.getInitializableTickIndex( + tickUpperIndex, + tickSpacing, + ); + if ( + !TickUtil.checkTickInBounds(tickLowerInitializableIndex) || + !TickUtil.checkTickInBounds(tickUpperInitializableIndex) + ) { + throw Error("Prices out of bounds"); + } + depositTokenAmount = isCorrectMintOrder + ? depositTokenAmount * Math.pow(10, mintAAccount.decimals) + : depositTokenAmount * Math.pow(10, mintBAccount.decimals); + const increasLiquidityQuoteParam: IncreaseLiquidityQuoteParam = { + inputTokenAmount: new BN(depositTokenAmount), + inputTokenMint: depositTokenMint, + tokenMintA: mintA, + tokenMintB: mintB, + tickCurrentIndex: initialTick, + sqrtPrice: initSqrtPrice, + tickLowerIndex: tickLowerInitializableIndex, + tickUpperIndex: tickUpperInitializableIndex, + tokenExtensionCtx: tokenExtensionCtx, + slippageTolerance: Percentage.fromFraction(0, 100), + }; + const liquidityInput = increaseLiquidityQuoteByInputTokenWithParams( + increasLiquidityQuoteParam, + ); + const { liquidityAmount: liquidity, tokenMaxA, tokenMaxB } = liquidityInput; - const baseParamsLiquidity = { - liquidityAmount: liquidity, - tokenMaxA, - tokenMaxB, - whirlpool: whirlpoolPda.publicKey, - positionAuthority: wallet.publicKey, - position: positionPda.publicKey, - positionTokenAccount: positionTokenAccountAddress, - tokenOwnerAccountA, - tokenOwnerAccountB, - tokenVaultA: tokenVaultAKeypair.publicKey, - tokenVaultB: tokenVaultBKeypair.publicKey, - tickArrayLower: tickArrayLowerPda.publicKey, - tickArrayUpper: tickArrayUpperPda.publicKey, - }; + const positionMintKeypair = Keypair.generate(); + const positionMintPubkey = positionMintKeypair.publicKey; + const positionPda = PDAUtil.getPosition( + ORCA_WHIRLPOOL_PROGRAM_ID, + positionMintPubkey, + ); + const positionTokenAccountAddress = getAssociatedTokenAddressSync( + positionMintPubkey, + wallet.publicKey, + ctx.accountResolverOpts.allowPDAOwnerAddress, + TOKEN_2022_PROGRAM_ID, + ); + const params = { + funder: wallet.publicKey, + owner: wallet.publicKey, + positionPda, + positionTokenAccount: positionTokenAccountAddress, + whirlpool: whirlpoolPda.publicKey, + tickLowerIndex: tickLowerInitializableIndex, + tickUpperIndex: tickUpperInitializableIndex, + }; + const positionIx = openPositionWithTokenExtensionsIx(ctx.program, { + ...params, + positionMint: positionMintPubkey, + withTokenMetadataExtension: true, + }); - const liquidityIx = !TokenExtensionUtil.isV2IxRequiredPool(tokenExtensionCtx) - ? increaseLiquidityIx(ctx.program, baseParamsLiquidity) - : increaseLiquidityV2Ix(ctx.program, { - ...baseParamsLiquidity, - tokenMintA: mintA, - tokenMintB: mintB, - tokenProgramA: tokenExtensionCtx.tokenMintWithProgramA.tokenProgram, - tokenProgramB: tokenExtensionCtx.tokenMintWithProgramB.tokenProgram, - }); - txBuilder.addInstruction(liquidityIx); + txBuilder.addInstruction(positionIx); + txBuilder.addSigner(positionMintKeypair); - const txPayload = await txBuilder.build(); - const instructions = TransactionMessage.decompile( - (txPayload.transaction as VersionedTransaction).message, - ).instructions; + const [ataA, ataB] = await resolveOrCreateATAs( + ctx.connection, + wallet.publicKey, + [ + { tokenMint: mintA, wrappedSolAmountIn: tokenMaxA }, + { tokenMint: mintB, wrappedSolAmountIn: tokenMaxB }, + ], + () => ctx.fetcher.getAccountRentExempt(), + wallet.publicKey, + undefined, + ctx.accountResolverOpts.allowPDAOwnerAddress, + "ata", + ); + const { address: tokenOwnerAccountA, ...tokenOwnerAccountAIx } = ataA; + const { address: tokenOwnerAccountB, ...tokenOwnerAccountBIx } = ataB; - try { + txBuilder.addInstruction(tokenOwnerAccountAIx); + txBuilder.addInstruction(tokenOwnerAccountBIx); + + const tickArrayLowerStartIndex = TickUtil.getStartTickIndex( + tickLowerInitializableIndex, + tickSpacing, + ); + const tickArrayUpperStartIndex = TickUtil.getStartTickIndex( + tickUpperInitializableIndex, + tickSpacing, + ); + const tickArrayLowerPda = PDAUtil.getTickArray( + ctx.program.programId, + whirlpoolPda.publicKey, + tickArrayLowerStartIndex, + ); + const tickArrayUpperPda = PDAUtil.getTickArray( + ctx.program.programId, + whirlpoolPda.publicKey, + tickArrayUpperStartIndex, + ); + if (tickArrayUpperStartIndex !== tickArrayLowerStartIndex) { + if (isCorrectMintOrder) { + txBuilder.addInstruction( + initTickArrayIx(ctx.program, { + startTick: tickArrayUpperStartIndex, + tickArrayPda: tickArrayUpperPda, + whirlpool: whirlpoolPda.publicKey, + funder: wallet.publicKey, + }), + ); + } else { + txBuilder.addInstruction( + initTickArrayIx(ctx.program, { + startTick: tickArrayLowerStartIndex, + tickArrayPda: tickArrayLowerPda, + whirlpool: whirlpoolPda.publicKey, + funder: wallet.publicKey, + }), + ); + } + } + + const baseParamsLiquidity = { + liquidityAmount: liquidity, + tokenMaxA, + tokenMaxB, + whirlpool: whirlpoolPda.publicKey, + positionAuthority: wallet.publicKey, + position: positionPda.publicKey, + positionTokenAccount: positionTokenAccountAddress, + tokenOwnerAccountA, + tokenOwnerAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tickArrayLower: tickArrayLowerPda.publicKey, + tickArrayUpper: tickArrayUpperPda.publicKey, + }; + + const liquidityIx = !TokenExtensionUtil.isV2IxRequiredPool(tokenExtensionCtx) + ? increaseLiquidityIx(ctx.program, baseParamsLiquidity) + : increaseLiquidityV2Ix(ctx.program, { + ...baseParamsLiquidity, + tokenMintA: mintA, + tokenMintB: mintB, + tokenProgramA: tokenExtensionCtx.tokenMintWithProgramA.tokenProgram, + tokenProgramB: tokenExtensionCtx.tokenMintWithProgramB.tokenProgram, + }); + txBuilder.addInstruction(liquidityIx); + + const txPayload = await txBuilder.build(); + const instructions = TransactionMessage.decompile( + (txPayload.transaction as VersionedTransaction).message, + ).instructions; + + const txId = await sendTx( agent, instructions, diff --git a/src/tools/orca_fetch_positions.ts b/src/tools/orca_fetch_positions.ts new file mode 100644 index 0000000..647d052 --- /dev/null +++ b/src/tools/orca_fetch_positions.ts @@ -0,0 +1,104 @@ +import { SolanaAgentKit } from "../agent"; +import { Wallet } from "@coral-xyz/anchor"; +import { + ORCA_WHIRLPOOL_PROGRAM_ID, + WhirlpoolContext, + buildWhirlpoolClient, + getAllPositionAccountsByOwner, + PriceMath, +} from "@orca-so/whirlpools-sdk"; + +interface PositionInfo { + whirlpoolAddress: string; + positionInRange: boolean; + distanceFromCenterBps: number; +} + +type PositionDataMap = { + [positionMintAddress: string]: PositionInfo; +}; + +/** + * # Fetches Liquidity Position Data in Orca Whirlpools + * + * Fetches data for all liquidity positions owned by the provided wallet, including: + * - Whirlpool address. + * - Whether the position is in range. + * - Distance from the center price to the current price in basis points. + * + * ## Parameters + * - `agent`: The `SolanaAgentKit` instance representing the wallet and connection. + * + * ## Returns + * A JSON string with an object mapping position mint addresses to position details: + * ```json + * { + * "positionMintAddress1": { + * "whirlpoolAddress": "whirlpoolAddress1", + * "positionInRange": true, + * "distanceFromCenterBps": 250 + * } + * } + * ``` + * + * ## Throws + * - If positions cannot be fetched or processed. + * - If the position mint address is invalid. + * + * @param agent - The `SolanaAgentKit` instance. + * @returns A JSON string with position data. + */ +export async function orcaFetchPositions( + agent: SolanaAgentKit, +): Promise { + try { + const wallet = new Wallet(agent.wallet); + const ctx = WhirlpoolContext.from( + agent.connection, + wallet, + ORCA_WHIRLPOOL_PROGRAM_ID, + ); + const client = buildWhirlpoolClient(ctx) + + const positions = await getAllPositionAccountsByOwner({ + ctx, + owner: agent.wallet.publicKey + }) + const positionDatas = [ + ...positions.positions.entries(), + ...positions.positionsWithTokenExtensions.entries() + ]; + const result: PositionDataMap = {}; + for (const [_, positionData] of positionDatas) { + const positionMintAddress = positionData.positionMint; + const whirlpoolAddress = positionData.whirlpool; + const whirlpool = await client.getPool(whirlpoolAddress); + const whirlpoolData = whirlpool.getData(); + const sqrtPrice = whirlpoolData.sqrtPrice; + const currentTick = whirlpoolData.tickCurrentIndex; + const mintA = whirlpool.getTokenAInfo(); + const mintB = whirlpool.getTokenBInfo(); + const currentPrice = PriceMath.sqrtPriceX64ToPrice(sqrtPrice, mintA.decimals, mintB.decimals); + const lowerTick = positionData.tickLowerIndex + const upperTick = positionData.tickUpperIndex + const lowerPrice = PriceMath.tickIndexToPrice(lowerTick, mintA.decimals, mintB.decimals); + const upperPrice = PriceMath.tickIndexToPrice(upperTick, mintA.decimals, mintB.decimals); + const centerPosition = (lowerPrice.add(upperPrice)).div(2); + + const positionInRange = (currentTick > lowerTick && currentTick < upperTick) ? true : false; + const distanceFromCenterBps = Math.ceil( + currentPrice.sub(centerPosition).abs().div(centerPosition).mul(10000).toNumber() + ); + + result[positionMintAddress.toString()] = { + whirlpoolAddress: whirlpoolAddress.toString(), + positionInRange, + distanceFromCenterBps, + }; + } + return JSON.stringify(result); + } catch (error) { + console.log(error) + throw new Error(`${error}`); + } +} \ No newline at end of file diff --git a/src/tools/orca_open_centered_position_with_liquidity.ts b/src/tools/orca_open_centered_position_with_liquidity.ts index de89a28..0fe3d0d 100644 --- a/src/tools/orca_open_centered_position_with_liquidity.ts +++ b/src/tools/orca_open_centered_position_with_liquidity.ts @@ -14,6 +14,7 @@ import { import { sendTx } from "../utils/send_tx"; import { Percentage } from "@orca-so/common-sdk"; +import { TOKEN_2022_PROGRAM_ID } from "@solana/spl-token"; /** * # Opens a Centered Liquidity Position in an Orca Whirlpool @@ -71,18 +72,19 @@ export async function orcaOpenCenteredPositionWithLiquidity( const client = buildWhirlpoolClient(ctx) const whirlpool = await client.getPool(whirlpoolAddress); + const whirlpoolData = whirlpool.getData(); const mintInfoA = whirlpool.getTokenAInfo() const mintInfoB = whirlpool.getTokenBInfo() const price = PriceMath.sqrtPriceX64ToPrice( - whirlpool.getData().sqrtPrice, + whirlpoolData.sqrtPrice, mintInfoA.decimals, mintInfoB.decimals ) const lowerPrice = price.mul(1 - priceOffsetBps / 10000) const upperPrice = price.mul(1 + priceOffsetBps / 10000) - const lowerTick = PriceMath.priceToTickIndex(lowerPrice, mintInfoA.decimals, mintInfoB.decimals) - const upperTick = PriceMath.priceToTickIndex(upperPrice, mintInfoA.decimals, mintInfoB.decimals) + const lowerTick = PriceMath.priceToInitializableTickIndex(lowerPrice, mintInfoA.decimals, mintInfoB.decimals, whirlpoolData.tickSpacing) + const upperTick = PriceMath.priceToInitializableTickIndex(upperPrice, mintInfoA.decimals, mintInfoB.decimals, whirlpoolData.tickSpacing) const txBuilderTickArrays = await whirlpool.initTickArrayForTicks([lowerTick, upperTick]) let instructions: TransactionInstruction[] = [] @@ -112,7 +114,11 @@ export async function orcaOpenCenteredPositionWithLiquidity( const { positionMint, tx: txBuilder } = await whirlpool.openPositionWithMetadata( lowerTick, upperTick, - increaseLiquiditQuote + increaseLiquiditQuote, + undefined, + undefined, + undefined, + TOKEN_2022_PROGRAM_ID ) const txPayload = await txBuilder.build(); diff --git a/src/tools/orca_open_single_sided_position.ts b/src/tools/orca_open_single_sided_position.ts index e69de29..1829865 100644 --- a/src/tools/orca_open_single_sided_position.ts +++ b/src/tools/orca_open_single_sided_position.ts @@ -0,0 +1,169 @@ +import { Keypair, PublicKey, TransactionInstruction, TransactionMessage, VersionedTransaction } from "@solana/web3.js"; +import { SolanaAgentKit } from "../agent"; +import { Wallet } from "@coral-xyz/anchor"; +import { Decimal } from "decimal.js"; +import { + ORCA_WHIRLPOOL_PROGRAM_ID, + WhirlpoolContext, + PriceMath, + buildWhirlpoolClient, + increaseLiquidityQuoteByInputToken, + TokenExtensionContextForPool, + NO_TOKEN_EXTENSION_CONTEXT, +} from "@orca-so/whirlpools-sdk"; +import { sendTx } from "../utils/send_tx"; +import { Percentage } from "@orca-so/common-sdk"; +import { TOKEN_2022_PROGRAM_ID } from "@solana/spl-token"; + +/** + * # Opens a Single-Sided Liquidity Position in an Orca Whirlpool + * + * This function opens a single-sided liquidity position in a specified Orca Whirlpool. The user specifies + * a basis point (bps) offset from the current price for the lower bound and a width (bps) for the range width. + * The required amount of the other token is calculated automatically. + * + * ## Parameters + * - `agent`: The `SolanaAgentKit` instance representing the wallet and connection details. + * - `whirlpoolAddress`: The address of the Orca Whirlpool where the position will be opened. + * - `distanceFromCurrentPriceBps`: The basis point offset from the current price for the lower bound. + * - `widthBps`: The width of the range as a percentage increment from the lower bound. + * - `inputTokenMint`: The mint address of the token being deposited (e.g., USDC or another token). + * - `inputAmount`: The amount of the input token to deposit, specified as a `Decimal` value. + * + * ## Returns + * A `Promise` that resolves to the transaction ID (`string`) of the transaction that opens the position. + * + * ## Notes + * - The `distanceFromCurrentPriceBps` specifies the starting point of the range. + * - The `widthBps` determines the range size from the lower bound. + * - The specified `inputTokenMint` determines which token is deposited directly. + * + * @param agent - The `SolanaAgentKit` instance representing the wallet and connection. + * @param whirlpoolAddress - The address of the Orca Whirlpool. + * @param distanceFromCurrentPriceBps - The basis point offset from the current price for the lower bound. + * @param widthBps - The width of the range as a percentage increment from the lower bound. + * @param inputTokenMint - The mint address of the token to deposit. + * @param inputAmount - The amount of the input token to deposit. + * @returns A promise resolving to the transaction ID (`string`). + */ +export async function orcaOpenSingleSidedPosition( + agent: SolanaAgentKit, + whirlpoolAddress: PublicKey, + distanceFromCurrentPriceBps: number, + widthBps: number, + inputTokenMint: PublicKey, + inputAmount: Decimal +): Promise { + try { + const wallet = new Wallet(agent.wallet); + const ctx = WhirlpoolContext.from( + agent.connection, + wallet, + ORCA_WHIRLPOOL_PROGRAM_ID, + ); + // ctx.accountResolverOpts.createWrappedSolAccountMethod = "ata"; + const client = buildWhirlpoolClient(ctx); + + const whirlpool = await client.getPool(whirlpoolAddress); + const whirlpoolData = whirlpool.getData(); + const mintInfoA = whirlpool.getTokenAInfo(); + const mintInfoB = whirlpool.getTokenBInfo(); + const price = PriceMath.sqrtPriceX64ToPrice( + whirlpoolData.sqrtPrice, + mintInfoA.decimals, + mintInfoB.decimals + ); + + const isTokenA = inputTokenMint.equals(mintInfoA.mint); + let lowerBoundPrice; + let upperBoundPrice; + let lowerTick; + let upperTick; + if (isTokenA) { + lowerBoundPrice = price.mul(1 + distanceFromCurrentPriceBps / 10000); + upperBoundPrice = lowerBoundPrice.mul(1 + widthBps / 10000); + upperTick = PriceMath.priceToInitializableTickIndex( + upperBoundPrice, + mintInfoA.decimals, + mintInfoB.decimals, + whirlpoolData.tickSpacing + ); + lowerTick = PriceMath.priceToInitializableTickIndex( + lowerBoundPrice, + mintInfoA.decimals, + mintInfoB.decimals, + whirlpoolData.tickSpacing + ); + } else { + lowerBoundPrice = price.mul(1 - distanceFromCurrentPriceBps / 10000); + upperBoundPrice = lowerBoundPrice.mul(1 - widthBps / 10000); + lowerTick = PriceMath.priceToInitializableTickIndex( + upperBoundPrice, + mintInfoA.decimals, + mintInfoB.decimals, + whirlpoolData.tickSpacing + ); + upperTick = PriceMath.priceToInitializableTickIndex( + lowerBoundPrice, + mintInfoA.decimals, + mintInfoB.decimals, + whirlpoolData.tickSpacing + ); + } + + const txBuilderTickArrays = await whirlpool.initTickArrayForTicks([lowerTick, upperTick]); + let txIds: string = ''; + if (txBuilderTickArrays !== null) { + const txPayloadTickArrays = await txBuilderTickArrays.build(); + const txPayloadTickArraysDecompiled = TransactionMessage.decompile((txPayloadTickArrays.transaction as VersionedTransaction).message); + const instructions = txPayloadTickArraysDecompiled.instructions; + const signers = txPayloadTickArrays.signers as Keypair[]; + + const tickArrayTxId = await sendTx(agent, instructions, signers); + txIds += tickArrayTxId + ','; + } + + const tokenExtensionCtx: TokenExtensionContextForPool = { + ...NO_TOKEN_EXTENSION_CONTEXT, + tokenMintWithProgramA: mintInfoA, + tokenMintWithProgramB: mintInfoB, + }; + const increaseLiquiditQuote = increaseLiquidityQuoteByInputToken( + inputTokenMint, + inputAmount, + lowerTick, + upperTick, + Percentage.fromFraction(1, 100), + whirlpool, + tokenExtensionCtx + ); + const { positionMint, tx: txBuilder } = await whirlpool.openPositionWithMetadata( + lowerTick, + upperTick, + increaseLiquiditQuote, + undefined, + undefined, + undefined, + TOKEN_2022_PROGRAM_ID + ); + + const txPayload = await txBuilder.build(); + const txPayloadDecompiled = TransactionMessage.decompile((txPayload.transaction as VersionedTransaction).message); + const instructions = txPayloadDecompiled.instructions; + const signers = txPayload.signers as Keypair[]; + for (const signer of signers) { + console.log(signer.publicKey.toBase58()); + } + + const positionTxId = await sendTx(agent, instructions, signers); + txIds += positionTxId; + + return JSON.stringify({ + transactionIds: txIds, + positionMint: positionMint.toString(), + }); + } catch (error) { + console.log(error); + throw new Error(`${error}`); + } +} diff --git a/src/utils/send_tx.ts b/src/utils/send_tx.ts index ad42204..d897b9a 100644 --- a/src/utils/send_tx.ts +++ b/src/utils/send_tx.ts @@ -54,7 +54,6 @@ export async function getComputeBudgetInstructions(agent: SolanaAgentKit, instru computeBudgetPriorityFeeInstructions }; } catch (error) { - console.error("Error getting compute budget instructions fees:", error); throw error; } } @@ -82,7 +81,7 @@ export async function sendTx( instructions: allInstructions, }).compileToV0Message(); const transaction = new VersionedTransaction(messageV0); - transaction.sign([agent.wallet, ...(otherKeypairs ?? [])]); + transaction.sign([agent.wallet, ...(otherKeypairs ?? [])] as Signer[]); const timeoutMs = 90000; const startTime = Date.now(); @@ -94,7 +93,7 @@ export async function sendTx( transaction, { maxRetries: 0, - skipPreflight: true, + skipPreflight: false, }); const statuses = await agent.connection.getSignatureStatuses([signature]); @@ -114,11 +113,9 @@ export async function sendTx( } throw new Error("Transaction timeout"); } catch (error) { - console.log("Error sending transaction:", error); throw error; } } catch (error) { - console.log("Error sending transaction:", error); throw error; } } diff --git a/test/index.ts b/test/index.ts index 1a64ec9..6fb00f4 100644 --- a/test/index.ts +++ b/test/index.ts @@ -37,7 +37,7 @@ async function initializeAgent() { try { const llm = new ChatOpenAI({ modelName: "gpt-4o-mini", - // temperature: 0.7, + temperature: 0.3, }); let walletDataStr: string | null = null;