Opening a Position

Positions in Whirlpools are tracked with a minted NFT in the user's wallet.

The usual action of opening a position consists of two instruction calls

Opening Position with Metadata

In either way of constructing a position, users have the option of appending Metaplex metadata onto the position NFT. Doing so will allow the token to be more identifiable in tracking websites or wallets as a Whirlpool NFT. The drawback is it will require more compute-budget.

To append, use openPositionWithMetadata. Otherwise, use openPosition.

Initialize Tick Array accounts if needed

For liquidity to exist in the Whirlpool, the tick-array that contains that particular tick must be initialized. Calculate the start_index of the required tick array and use the initialize_tick_array instruction to initialize it.

More often than not, tick-arrays are already created. But if you want your code to be defensive, you should do a check prior to invoking open_position. To understand more on how Tick-Arrays work in Whirlpools, read here.

// TODO: add tick-array code sample

const tickArrayPda = PDAUtil.getTickArray(
  this.ctx.program.programId,
  this.address,
  startTick
);

// Check if tick array exists
const fetcher = new AccountFetcher(...);
const ta = await fetcher.getTickArray(tickArrayPda.publicKey, true);
// Exit if it exists
if (!!ta) {
  return;
}

// Construct Init Tick Array Ix
const tx = toTx(ctx, WhirlpoolIx.initTickArrayIx(this.ctx.program, {
  startTick,
  tickArrayPda,
  whirlpool: this.address,
  funder: !!funder ? AddressUtil.toPubKey(funder) : this.ctx.wallet.publicKey,
}));
await tx.buildAndExecute();

Open Position with WhirlpoolClient

WhirlpoolClient's openPosition method bundles the open and increase liquidity instructions into a single transaction for you. Below is a code sample to create a position for the SOL/USDC pool at the price between $98 - $150, with the intention to deposit 50 SOL into the position.

// Derive the Whirlpool address
const poolAddress = PDAUtil.getWhirlpool(
    WHIRLPOOL_PROGRAM_ID,
    ORCA_WHIRLPOOLS_CONFIG,
    SOL_MINT,
    USDC_MINT,
    64
  );

// Load everything that you need
const client = buildWhirlpoolClient(context, fetcher);
const pool = await client.getPool(poolAddress.publicKey);
const poolData = pool.getData();
const poolTokenAInfo = pool.getTokenAInfo();
const poolTokenBInfo = pool.getTokenBInfo();

// Derive the tick-indices based on a human-readable price
const tokenADecimal = poolTokenAInfo.decimals;
const tokenBDecimal = poolTokenBInfo.decimals;
const tickLower = TickUtil.getInitializableTickIndex(
  PriceMath.priceToTickIndex(new Decimal(98), tokenADecimal, tokenBDecimal),
  poolData.tickSpacing
);
const tickUpper = TickUtil.getInitializableTickIndex(
  PriceMath.priceToTickIndex(new Decimal(150), tokenADecimal, tokenBDecimal),
  poolData.tickSpacing
);

// Get a quote on the estimated liquidity and tokenIn (50 tokenA)
const quote = increaseLiquidityQuoteByInputToken(
  poolTokenAInfo.mint,
  new Decimal(50),
  tickLower,
  tickUpper,
  Percentage.fromFraction(1, 100),
  pool
);

// Evaluate the quote if you need
const {tokenMaxA, tokenMaxB} = quote

// Construct the open position & increase_liquidity ix and execute the transaction.
const { positionMint, tx } = await pool.openPosition(
  lowerTick,
  upperTick,
  quote
);
const txId = await tx.buildAndExecute();

// Fetch the newly created position with liquidity
const position = await client.getPosition(
  PDAUtil.getPosition(WHIRLPOOL_PROGRAM_ID, positionMint).publicKey
)

The Manual way

Follow the instructions below if you would like to have more control over your instruction building process. Note that open_position does not add liquidity to a position. Follow the next article "Modify Liquidity" to add liquidity.

Determine position parameters

To open a position against a Whirlpool, you must first define certain parameters of your position to invoke the open_position instruction.

WhirlpoolKey - The public key for the Whirlpool that the position will host liquidity in.

tickLowerIndex, tickUpperIndex - The tick index bounds for the position. Must be an initializable index.

positionMintAddress - A generated empty Keypair that will be initialized to a token mint.

positionPda - Derived address of the position account via getPositionPda

positionTokenAccountAddress - This is the account that will hold the minted position token. It is the associated token address of the position-mint.

Sample Code

    const positionMintKeypair = Keypair.generate();
    const positionPda = getPositionPda(programId, positionMintKeypair.publicKey);
    const metadataPda = getPositionMetadataPda(positionMintKeypair.publicKey);
    const positionTokenAccountAddress = await deriveATA(
      provider.wallet.publicKey,
      positionMintKeypair.publicKey
    );

    const positionIx = toTx(ctx, WhirlpoolIx.openPositionWithMetadataIx(ctx.program, {
        funder: provider.wallet.publicKey,
        ownerKey: provider.wallet.publicKey,
        positionPda,
        metadataPda,
        positionMintAddress: positionMintKeypair.publicKey,
        positionTokenAccountAddress,
        whirlpoolKey: toPubKey(poolAddress),
        tickLowerIndex,
        tickUpperIndex,
      })).addSigner(positionMintKeypair).buildAndExecute();

Once your position is open, proceed to the next section to add liquidity.

Common Errors

  • InvalidTickIndex (0x177a)

    • tickLowerIndex is higher than upper tickUpperIndex

    • Some tick indices is not an initializable index (not a multiple of tickSpacing). Use TickUtil.getInitializableTickIndex to get the closest initializable tick to your index.

    • Some tick indices is out of bounds

  • NotRentExempt (0x0)

    • Usually, the TickArray that houses your tickLowerIndex or tickUpperIndex has not been initialized. Use the WhirlpoolClient.initTickArrayForTicks or WhirlpoolIx.initTickArrayIx to initialize the array at the derived startTickIndex.

    • Alternatively, if this failure is from init_tick_array, the tick array has already been initialized.

Last updated