import { Keypair, PublicKey, TransactionInstruction, TransactionMessage, VersionedTransaction, } from "@solana/web3.js"; import { SolanaAgentKit } from "../agent"; import { Wallet } from "../utils/keypair"; 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 Centered Liquidity Position in an Orca Whirlpool * * This function opens a centered liquidity position in a specified Orca Whirlpool. The user defines * a basis point (bps) offset from the current price of the pool to set the lower and upper bounds of the position. * The user also specifies the token mint and the amount to deposit. 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. * - `priceOffsetBps`: The basis point (bps) offset (on one side) from the current price fo the pool. For example, * 500 bps (5%) creates a range from 95% to 105% of the current pool price. * - `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 `priceOffsetBps` specifies the range symmetrically around the current price. * - The specified `inputTokenMint` determines which token is deposited directly. The function calculates * the required amount of the other token based on the specified price range. * - This function supports Orca's token extensions for managing tokens with special behaviors. * - The function assumes a maximum slippage of 1% for liquidity provision. * * ## Throws * An error will be thrown if: * - The specified Whirlpool address is invalid or inaccessible. * - The transaction fails to send. * - Any required mint information cannot be fetched. * * @param agent - The `SolanaAgentKit` instance representing the wallet and connection. * @param whirlpoolAddress - The address of the Orca Whirlpool. * @param priceOffsetBps - The basis point offset (one side) from the current pool price. * @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 orcaOpenCenteredPositionWithLiquidity( agent: SolanaAgentKit, whirlpoolAddress: PublicKey, priceOffsetBps: number, inputTokenMint: PublicKey, inputAmount: Decimal, ): Promise { try { const wallet = new Wallet(agent.wallet); const ctx = WhirlpoolContext.from( agent.connection, wallet, ORCA_WHIRLPOOL_PROGRAM_ID, ); 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 lowerPrice = price.mul(1 - priceOffsetBps / 10000); const upperPrice = price.mul(1 + priceOffsetBps / 10000); 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[] = []; let signers: Keypair[] = []; if (txBuilderTickArrays !== null) { const txPayloadTickArrays = await txBuilderTickArrays.build(); const txPayloadTickArraysDecompiled = TransactionMessage.decompile( (txPayloadTickArrays.transaction as VersionedTransaction).message, ); const instructionsTickArrays = txPayloadTickArraysDecompiled.instructions; instructions = instructions.concat(instructionsTickArrays); signers = signers.concat(txPayloadTickArrays.signers as Keypair[]); } 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, ); instructions = instructions.concat(txPayloadDecompiled.instructions); signers = signers.concat(txPayload.signers as Keypair[]); const txId = await sendTx(agent, instructions, signers); return JSON.stringify({ transactionId: txId, positionMint: positionMint.toString(), }); } catch (error) { throw new Error(`${error}`); } }