[v0.2.0] Improve increase/decrease liquidity functions for SDK (#16)
- Add support for auto-resolving ATA accounts prior for decrease liquidity ix - Removed sourceWallet support for increase_liquidity ix - Add separate funder/payer support for Whirlpool/Position increase/decrease liquidity functions - Add tests to verify functions work as intended
This commit is contained in:
parent
efe85aa44d
commit
f888d1cf20
|
@ -7,7 +7,7 @@
|
|||
"types": "dist/index.d.ts",
|
||||
"dependencies": {
|
||||
"@metaplex-foundation/mpl-token-metadata": "1.2.5",
|
||||
"@orca-so/common-sdk": "^0.0.4",
|
||||
"@orca-so/common-sdk": "^0.0.6",
|
||||
"@project-serum/anchor": "^0.20.1",
|
||||
"@solana/spl-token": "^0.1.8",
|
||||
"decimal.js": "^10.3.1",
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import { AddressUtil, deriveATA, TransactionBuilder } from "@orca-so/common-sdk";
|
||||
import {
|
||||
AddressUtil,
|
||||
deriveATA,
|
||||
resolveOrCreateATAs,
|
||||
TransactionBuilder,
|
||||
} from "@orca-so/common-sdk";
|
||||
import { Address } from "@project-serum/anchor";
|
||||
import { WhirlpoolContext } from "../context";
|
||||
import {
|
||||
|
@ -54,36 +59,42 @@ export class PositionImpl implements Position {
|
|||
throw new Error("Unable to fetch whirlpool for this position.");
|
||||
}
|
||||
|
||||
return toTx(
|
||||
this.ctx,
|
||||
increaseLiquidityIx(this.ctx.program, {
|
||||
...liquidityInput,
|
||||
whirlpool: this.data.whirlpool,
|
||||
position: this.address,
|
||||
positionTokenAccount: await deriveATA(positionWalletKey, this.data.positionMint),
|
||||
tokenOwnerAccountA: await deriveATA(sourceWalletKey, whirlpool.tokenMintA),
|
||||
tokenOwnerAccountB: await deriveATA(sourceWalletKey, whirlpool.tokenMintB),
|
||||
tokenVaultA: whirlpool.tokenVaultA,
|
||||
tokenVaultB: whirlpool.tokenVaultB,
|
||||
tickArrayLower: PDAUtil.getTickArray(
|
||||
this.ctx.program.programId,
|
||||
this.data.whirlpool,
|
||||
TickUtil.getStartTickIndex(this.data.tickLowerIndex, whirlpool.tickSpacing)
|
||||
).publicKey,
|
||||
tickArrayUpper: PDAUtil.getTickArray(
|
||||
this.ctx.program.programId,
|
||||
this.data.whirlpool,
|
||||
TickUtil.getStartTickIndex(this.data.tickUpperIndex, whirlpool.tickSpacing)
|
||||
).publicKey,
|
||||
positionAuthority: positionWalletKey,
|
||||
})
|
||||
);
|
||||
const txBuilder = new TransactionBuilder(this.ctx.provider);
|
||||
const tokenOwnerAccountA = await deriveATA(sourceWalletKey, whirlpool.tokenMintA);
|
||||
const tokenOwnerAccountB = await deriveATA(sourceWalletKey, whirlpool.tokenMintB);
|
||||
const positionTokenAccount = await deriveATA(positionWalletKey, this.data.positionMint);
|
||||
|
||||
const increaseIx = increaseLiquidityIx(this.ctx.program, {
|
||||
...liquidityInput,
|
||||
whirlpool: this.data.whirlpool,
|
||||
position: this.address,
|
||||
positionTokenAccount,
|
||||
tokenOwnerAccountA,
|
||||
tokenOwnerAccountB,
|
||||
tokenVaultA: whirlpool.tokenVaultA,
|
||||
tokenVaultB: whirlpool.tokenVaultB,
|
||||
tickArrayLower: PDAUtil.getTickArray(
|
||||
this.ctx.program.programId,
|
||||
this.data.whirlpool,
|
||||
TickUtil.getStartTickIndex(this.data.tickLowerIndex, whirlpool.tickSpacing)
|
||||
).publicKey,
|
||||
tickArrayUpper: PDAUtil.getTickArray(
|
||||
this.ctx.program.programId,
|
||||
this.data.whirlpool,
|
||||
TickUtil.getStartTickIndex(this.data.tickUpperIndex, whirlpool.tickSpacing)
|
||||
).publicKey,
|
||||
positionAuthority: positionWalletKey,
|
||||
});
|
||||
txBuilder.addInstruction(increaseIx);
|
||||
return txBuilder;
|
||||
}
|
||||
|
||||
async decreaseLiquidity(
|
||||
liquidityInput: DecreaseLiquidityInput,
|
||||
sourceWallet?: Address,
|
||||
positionWallet?: Address
|
||||
positionWallet?: Address,
|
||||
resolveATA?: boolean,
|
||||
ataPayer?: Address
|
||||
) {
|
||||
const sourceWalletKey = sourceWallet
|
||||
? AddressUtil.toPubKey(sourceWallet)
|
||||
|
@ -91,36 +102,59 @@ export class PositionImpl implements Position {
|
|||
const positionWalletKey = positionWallet
|
||||
? AddressUtil.toPubKey(positionWallet)
|
||||
: this.ctx.wallet.publicKey;
|
||||
const ataPayerKey = ataPayer ? AddressUtil.toPubKey(ataPayer) : this.ctx.wallet.publicKey;
|
||||
const whirlpool = await this.fetcher.getPool(this.data.whirlpool, true);
|
||||
|
||||
if (!whirlpool) {
|
||||
throw new Error("Unable to fetch whirlpool for this position.");
|
||||
}
|
||||
|
||||
return toTx(
|
||||
this.ctx,
|
||||
decreaseLiquidityIx(this.ctx.program, {
|
||||
...liquidityInput,
|
||||
whirlpool: this.data.whirlpool,
|
||||
position: this.address,
|
||||
positionTokenAccount: await deriveATA(positionWalletKey, this.data.positionMint),
|
||||
tokenOwnerAccountA: await deriveATA(sourceWalletKey, whirlpool.tokenMintA),
|
||||
tokenOwnerAccountB: await deriveATA(sourceWalletKey, whirlpool.tokenMintB),
|
||||
tokenVaultA: whirlpool.tokenVaultA,
|
||||
tokenVaultB: whirlpool.tokenVaultB,
|
||||
tickArrayLower: PDAUtil.getTickArray(
|
||||
this.ctx.program.programId,
|
||||
this.data.whirlpool,
|
||||
TickUtil.getStartTickIndex(this.data.tickLowerIndex, whirlpool.tickSpacing)
|
||||
).publicKey,
|
||||
tickArrayUpper: PDAUtil.getTickArray(
|
||||
this.ctx.program.programId,
|
||||
this.data.whirlpool,
|
||||
TickUtil.getStartTickIndex(this.data.tickUpperIndex, whirlpool.tickSpacing)
|
||||
).publicKey,
|
||||
positionAuthority: positionWalletKey,
|
||||
})
|
||||
);
|
||||
const txBuilder = new TransactionBuilder(this.ctx.provider);
|
||||
let tokenOwnerAccountA: PublicKey;
|
||||
let tokenOwnerAccountB: PublicKey;
|
||||
|
||||
if (resolveATA) {
|
||||
const [ataA, ataB] = await resolveOrCreateATAs(
|
||||
this.ctx.connection,
|
||||
sourceWalletKey,
|
||||
[{ tokenMint: whirlpool.tokenMintA }, { tokenMint: whirlpool.tokenMintB }],
|
||||
() => this.fetcher.getAccountRentExempt(),
|
||||
ataPayerKey
|
||||
);
|
||||
const { address: ataAddrA, ...tokenOwnerAccountAIx } = ataA!;
|
||||
const { address: ataAddrB, ...tokenOwnerAccountBIx } = ataB!;
|
||||
tokenOwnerAccountA = ataAddrA;
|
||||
tokenOwnerAccountB = ataAddrB;
|
||||
txBuilder.addInstruction(tokenOwnerAccountAIx);
|
||||
txBuilder.addInstruction(tokenOwnerAccountBIx);
|
||||
} else {
|
||||
tokenOwnerAccountA = await deriveATA(sourceWalletKey, whirlpool.tokenMintA);
|
||||
tokenOwnerAccountB = await deriveATA(sourceWalletKey, whirlpool.tokenMintB);
|
||||
}
|
||||
|
||||
const decreaseIx = decreaseLiquidityIx(this.ctx.program, {
|
||||
...liquidityInput,
|
||||
whirlpool: this.data.whirlpool,
|
||||
position: this.address,
|
||||
positionTokenAccount: await deriveATA(positionWalletKey, this.data.positionMint),
|
||||
tokenOwnerAccountA,
|
||||
tokenOwnerAccountB,
|
||||
tokenVaultA: whirlpool.tokenVaultA,
|
||||
tokenVaultB: whirlpool.tokenVaultB,
|
||||
tickArrayLower: PDAUtil.getTickArray(
|
||||
this.ctx.program.programId,
|
||||
this.data.whirlpool,
|
||||
TickUtil.getStartTickIndex(this.data.tickLowerIndex, whirlpool.tickSpacing)
|
||||
).publicKey,
|
||||
tickArrayUpper: PDAUtil.getTickArray(
|
||||
this.ctx.program.programId,
|
||||
this.data.whirlpool,
|
||||
TickUtil.getStartTickIndex(this.data.tickUpperIndex, whirlpool.tickSpacing)
|
||||
).publicKey,
|
||||
positionAuthority: positionWalletKey,
|
||||
});
|
||||
txBuilder.addInstruction(decreaseIx);
|
||||
return txBuilder;
|
||||
}
|
||||
|
||||
private async refresh() {
|
||||
|
|
|
@ -65,8 +65,7 @@ export class WhirlpoolImpl implements Whirlpool {
|
|||
tickLower: number,
|
||||
tickUpper: number,
|
||||
liquidityInput: IncreaseLiquidityInput,
|
||||
sourceWallet?: Address,
|
||||
positionWallet?: Address,
|
||||
wallet?: Address,
|
||||
funder?: Address
|
||||
) {
|
||||
await this.refresh();
|
||||
|
@ -74,8 +73,7 @@ export class WhirlpoolImpl implements Whirlpool {
|
|||
tickLower,
|
||||
tickUpper,
|
||||
liquidityInput,
|
||||
!!sourceWallet ? AddressUtil.toPubKey(sourceWallet) : this.ctx.wallet.publicKey,
|
||||
!!positionWallet ? AddressUtil.toPubKey(positionWallet) : this.ctx.wallet.publicKey,
|
||||
!!wallet ? AddressUtil.toPubKey(wallet) : this.ctx.wallet.publicKey,
|
||||
!!funder ? AddressUtil.toPubKey(funder) : this.ctx.wallet.publicKey
|
||||
);
|
||||
}
|
||||
|
@ -94,7 +92,6 @@ export class WhirlpoolImpl implements Whirlpool {
|
|||
tickUpper,
|
||||
liquidityInput,
|
||||
!!sourceWallet ? AddressUtil.toPubKey(sourceWallet) : this.ctx.wallet.publicKey,
|
||||
!!positionWallet ? AddressUtil.toPubKey(positionWallet) : this.ctx.wallet.publicKey,
|
||||
!!funder ? AddressUtil.toPubKey(funder) : this.ctx.wallet.publicKey,
|
||||
true
|
||||
);
|
||||
|
@ -133,7 +130,8 @@ export class WhirlpoolImpl implements Whirlpool {
|
|||
positionAddress: Address,
|
||||
slippageTolerance: Percentage,
|
||||
destinationWallet?: Address,
|
||||
positionWallet?: Address
|
||||
positionWallet?: Address,
|
||||
payer?: Address
|
||||
) {
|
||||
await this.refresh();
|
||||
const positionWalletKey = positionWallet
|
||||
|
@ -142,11 +140,13 @@ export class WhirlpoolImpl implements Whirlpool {
|
|||
const destinationWalletKey = destinationWallet
|
||||
? AddressUtil.toPubKey(destinationWallet)
|
||||
: this.ctx.wallet.publicKey;
|
||||
const payerKey = payer ? AddressUtil.toPubKey(payer) : this.ctx.wallet.publicKey;
|
||||
return this.getClosePositionIx(
|
||||
AddressUtil.toPubKey(positionAddress),
|
||||
slippageTolerance,
|
||||
destinationWalletKey,
|
||||
positionWalletKey
|
||||
positionWalletKey,
|
||||
payerKey
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -164,8 +164,7 @@ export class WhirlpoolImpl implements Whirlpool {
|
|||
tickLower: number,
|
||||
tickUpper: number,
|
||||
liquidityInput: IncreaseLiquidityInput,
|
||||
sourceWallet: PublicKey,
|
||||
positionWallet: PublicKey,
|
||||
wallet: PublicKey,
|
||||
funder: PublicKey,
|
||||
withMetadata: boolean = false
|
||||
): Promise<{ positionMint: PublicKey; tx: TransactionBuilder }> {
|
||||
|
@ -196,10 +195,7 @@ export class WhirlpoolImpl implements Whirlpool {
|
|||
positionMintKeypair.publicKey
|
||||
);
|
||||
const metadataPda = PDAUtil.getPositionMetadata(positionMintKeypair.publicKey);
|
||||
const positionTokenAccountAddress = await deriveATA(
|
||||
positionWallet,
|
||||
positionMintKeypair.publicKey
|
||||
);
|
||||
const positionTokenAccountAddress = await deriveATA(wallet, positionMintKeypair.publicKey);
|
||||
|
||||
const txBuilder = new TransactionBuilder(this.ctx.provider);
|
||||
|
||||
|
@ -207,7 +203,7 @@ export class WhirlpoolImpl implements Whirlpool {
|
|||
this.ctx.program,
|
||||
{
|
||||
funder,
|
||||
owner: positionWallet,
|
||||
owner: wallet,
|
||||
positionPda,
|
||||
metadataPda,
|
||||
positionMintAddress: positionMintKeypair.publicKey,
|
||||
|
@ -221,12 +217,13 @@ export class WhirlpoolImpl implements Whirlpool {
|
|||
|
||||
const [ataA, ataB] = await resolveOrCreateATAs(
|
||||
this.ctx.connection,
|
||||
sourceWallet,
|
||||
wallet,
|
||||
[
|
||||
{ tokenMint: whirlpool.tokenMintA, wrappedSolAmountIn: tokenMaxA },
|
||||
{ tokenMint: whirlpool.tokenMintB, wrappedSolAmountIn: tokenMaxB },
|
||||
],
|
||||
() => this.fetcher.getAccountRentExempt()
|
||||
() => this.fetcher.getAccountRentExempt(),
|
||||
funder
|
||||
);
|
||||
const { address: tokenOwnerAccountA, ...tokenOwnerAccountAIx } = ataA;
|
||||
const { address: tokenOwnerAccountB, ...tokenOwnerAccountBIx } = ataB;
|
||||
|
@ -260,7 +257,7 @@ export class WhirlpoolImpl implements Whirlpool {
|
|||
tokenMaxA,
|
||||
tokenMaxB,
|
||||
whirlpool: this.address,
|
||||
positionAuthority: positionWallet,
|
||||
positionAuthority: wallet,
|
||||
position: positionPda.publicKey,
|
||||
positionTokenAccount: positionTokenAccountAddress,
|
||||
tokenOwnerAccountA,
|
||||
|
@ -282,7 +279,8 @@ export class WhirlpoolImpl implements Whirlpool {
|
|||
positionAddress: PublicKey,
|
||||
slippageTolerance: Percentage,
|
||||
destinationWallet: PublicKey,
|
||||
positionWallet: PublicKey
|
||||
positionWallet: PublicKey,
|
||||
payerKey: PublicKey
|
||||
): Promise<TransactionBuilder> {
|
||||
const position = await this.fetcher.getPosition(positionAddress, true);
|
||||
if (!position) {
|
||||
|
@ -317,7 +315,8 @@ export class WhirlpoolImpl implements Whirlpool {
|
|||
this.ctx.connection,
|
||||
destinationWallet,
|
||||
[{ tokenMint: whirlpool.tokenMintA }, { tokenMint: whirlpool.tokenMintB }],
|
||||
() => this.fetcher.getAccountRentExempt()
|
||||
() => this.fetcher.getAccountRentExempt(),
|
||||
payerKey
|
||||
);
|
||||
|
||||
const { address: tokenOwnerAccountA, ...createTokenOwnerAccountAIx } = ataA;
|
||||
|
@ -367,8 +366,8 @@ export class WhirlpoolImpl implements Whirlpool {
|
|||
|
||||
/* Close position */
|
||||
const positionIx = closePositionIx(this.ctx.program, {
|
||||
positionAuthority: this.ctx.wallet.publicKey,
|
||||
receiver: this.ctx.wallet.publicKey,
|
||||
positionAuthority: positionWallet,
|
||||
receiver: destinationWallet,
|
||||
positionTokenAccount,
|
||||
position: positionAddress,
|
||||
positionMint: position.positionMint,
|
||||
|
|
|
@ -109,13 +109,12 @@ export interface Whirlpool {
|
|||
*
|
||||
* User has to ensure the TickArray for tickLower and tickUpper has been initialized prior to calling this function.
|
||||
*
|
||||
* If `funder` is provided, the funder wallet has to sign this transaction.
|
||||
* If `wallet` or `funder` is provided, those wallets have to sign this transaction.
|
||||
*
|
||||
* @param tickLower - the tick index for the lower bound of this position
|
||||
* @param tickUpper - the tick index for the upper bound of this position
|
||||
* @param liquidityInput - an InputLiquidityInput type to define the desired liquidity amount to deposit
|
||||
* @param sourceWallet - optional - the wallet to withdraw tokens to deposit into the position. If null, the WhirlpoolContext wallet is used.
|
||||
* @param positionWallet - optional - the wallet to that houses the position token. If null, the WhirlpoolContext wallet is used.
|
||||
* @param wallet - optional - the wallet to withdraw tokens to deposit into the position and house the position token. If null, the WhirlpoolContext wallet is used.
|
||||
* @param funder - optional - the wallet that will fund the cost needed to initialize the position. If null, the WhirlpoolContext wallet is used.
|
||||
* @return `positionMint` - the position to be created. `tx` - The transaction containing the instructions to perform the operation on chain.
|
||||
*/
|
||||
|
@ -123,8 +122,7 @@ export interface Whirlpool {
|
|||
tickLower: number,
|
||||
tickUpper: number,
|
||||
liquidityInput: IncreaseLiquidityInput,
|
||||
sourceWallet?: Address,
|
||||
positionWallet?: Address,
|
||||
wallet?: Address,
|
||||
funder?: Address
|
||||
) => Promise<{ positionMint: PublicKey; tx: TransactionBuilder }>;
|
||||
|
||||
|
@ -133,13 +131,12 @@ export interface Whirlpool {
|
|||
*
|
||||
* User has to ensure the TickArray for tickLower and tickUpper has been initialized prior to calling this function.
|
||||
*
|
||||
* If `sourceWallet`, `positionWallet` or `funder` is provided, the wallet owners have to sign this transaction.
|
||||
* If `wallet` or `funder` is provided, the wallet owners have to sign this transaction.
|
||||
*
|
||||
* @param tickLower - the tick index for the lower bound of this position
|
||||
* @param tickUpper - the tick index for the upper bound of this position
|
||||
* @param liquidityInput - input that defines the desired liquidity amount and maximum tokens willing to be to deposited.
|
||||
* @param sourceWallet - optional - the wallet to withdraw tokens to deposit into the position. If null, the WhirlpoolContext wallet is used.
|
||||
* @param positionWallet - optional - the wallet to that houses the position token. If null, the WhirlpoolContext wallet is used.
|
||||
* @param wallet - optional - the wallet to withdraw tokens to deposit into the position and house the position token. If null, the WhirlpoolContext wallet is used.
|
||||
* @param funder - optional - the wallet that will fund the cost needed to initialize the position. If null, the WhirlpoolContext wallet is used.
|
||||
* @return `positionMint` - the position to be created. `tx` - The transaction containing the instructions to perform the operation on chain.
|
||||
*/
|
||||
|
@ -147,8 +144,7 @@ export interface Whirlpool {
|
|||
tickLower: number,
|
||||
tickUpper: number,
|
||||
liquidityInput: IncreaseLiquidityInput,
|
||||
sourceWallet?: Address,
|
||||
positionWallet?: Address,
|
||||
wallet?: Address,
|
||||
funder?: Address
|
||||
) => Promise<{ positionMint: PublicKey; tx: TransactionBuilder }>;
|
||||
|
||||
|
@ -157,18 +153,20 @@ export interface Whirlpool {
|
|||
*
|
||||
* Users have to collect all fees and rewards from this position prior to closing the account.
|
||||
*
|
||||
* If `positionWallet` is provided, the wallet owner has to sign this transaction.
|
||||
* If `positionWallet`, `payer` is provided, the wallet owner has to sign this transaction.
|
||||
*
|
||||
* @param positionAddress - The address of the position account.
|
||||
* @param slippageTolerance - The amount of slippage the caller is willing to accept when withdrawing liquidity.
|
||||
* @param destinationWallet - optional - The wallet that the tokens withdrawn will be sent to. If null, the WhirlpoolContext wallet is used.
|
||||
* @param destinationWallet - optional - The wallet that the tokens withdrawn and rent lamports will be sent to. If null, the WhirlpoolContext wallet is used.
|
||||
* @param positionWallet - optional - The wallet that houses the position token that corresponds to this position address. If null, the WhirlpoolContext wallet is used.
|
||||
* @param payer - optional - the wallet that will fund the cost needed to initialize the token ATA accounts. If null, the WhirlpoolContext wallet is used.
|
||||
*/
|
||||
closePosition: (
|
||||
positionAddress: Address,
|
||||
slippageTolerance: Percentage,
|
||||
destinationWallet?: Address,
|
||||
positionWallet?: Address
|
||||
positionWallet?: Address,
|
||||
payer?: Address
|
||||
) => Promise<TransactionBuilder>;
|
||||
|
||||
/**
|
||||
|
@ -206,18 +204,16 @@ export interface Position {
|
|||
|
||||
/**
|
||||
* Deposit additional tokens into this postiion.
|
||||
*
|
||||
* If `sourceWallet`, `positionWallet` is provided, the wallet owners have to sign this transaction.
|
||||
* The wallet must contain the position token and the necessary token A & B to complete the deposit.
|
||||
* If `wallet` is provided, the wallet owners have to sign this transaction.
|
||||
*
|
||||
* @param liquidityInput - input that defines the desired liquidity amount and maximum tokens willing to be to deposited.
|
||||
* @param sourceWallet - optional - the wallet to withdraw tokens to deposit into the position. If null, the WhirlpoolContext wallet is used.
|
||||
* @param positionWallet - optional - the wallet to that houses the position token. If null, the WhirlpoolContext wallet is used.
|
||||
* @param wallet - the wallet to withdraw tokens to deposit into the position. If null, the WhirlpoolContext wallet is used.
|
||||
* @return the transaction that will deposit the tokens into the position when executed.
|
||||
*/
|
||||
increaseLiquidity: (
|
||||
liquidityInput: IncreaseLiquidityInput,
|
||||
sourceWallet?: Address,
|
||||
positionWallet?: Address
|
||||
wallet?: Address
|
||||
) => Promise<TransactionBuilder>;
|
||||
|
||||
/**
|
||||
|
@ -226,14 +222,18 @@ export interface Position {
|
|||
* If `positionWallet` is provided, the wallet owners have to sign this transaction.
|
||||
*
|
||||
* @param liquidityInput - input that defines the desired liquidity amount and minimum tokens willing to be to withdrawn from the position.
|
||||
* @param sourceWallet - optional - the wallet to deposit tokens into when withdrawing from the position. If null, the WhirlpoolContext wallet is used.
|
||||
* @param destinationWallet - optional - the wallet to deposit tokens into when withdrawing from the position. If null, the WhirlpoolContext wallet is used.
|
||||
* @param positionWallet - optional - the wallet to that houses the position token. If null, the WhirlpoolContext wallet is used.
|
||||
* @param resolveATA - optional - if true, add instructions to create associated token accounts for tokenA,B for the destinationWallet if necessary.
|
||||
* @param ataPayer - optional - wallet that will fund the creation of the new associated token accounts
|
||||
* @return the transaction that will deposit the tokens into the position when executed.
|
||||
*/
|
||||
decreaseLiquidity: (
|
||||
liquidityInput: DecreaseLiquidityInput,
|
||||
sourceWallet?: Address,
|
||||
positionWallet?: Address
|
||||
destinationWallet?: Address,
|
||||
positionWallet?: Address,
|
||||
resolveATA?: boolean,
|
||||
ataPayer?: Address
|
||||
) => Promise<TransactionBuilder>;
|
||||
|
||||
// TODO: Implement Collect fees
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as anchor from "@project-serum/anchor";
|
|||
import * as assert from "assert";
|
||||
import { WhirlpoolContext } from "../../../src/context";
|
||||
import { initTestPool } from "../../utils/init-utils";
|
||||
import { TickSpacing } from "../../utils";
|
||||
import { createAssociatedTokenAccount, TickSpacing, transfer } from "../../utils";
|
||||
import {
|
||||
AccountFetcher,
|
||||
buildWhirlpoolClient,
|
||||
|
@ -11,7 +11,7 @@ import {
|
|||
PriceMath,
|
||||
} from "../../../src";
|
||||
import Decimal from "decimal.js";
|
||||
import { Percentage } from "@orca-so/common-sdk";
|
||||
import { deriveATA, Percentage } from "@orca-so/common-sdk";
|
||||
import { initPosition, mintTokensToTestAccount } from "../../utils/test-builders";
|
||||
|
||||
describe("position-impl", () => {
|
||||
|
@ -79,7 +79,7 @@ describe("position-impl", () => {
|
|||
);
|
||||
|
||||
await (
|
||||
await position.increaseLiquidity(increase_quote, ctx.wallet.publicKey, ctx.wallet.publicKey)
|
||||
await position.increaseLiquidity(increase_quote, ctx.wallet.publicKey)
|
||||
).buildAndExecute();
|
||||
|
||||
const postIncreaseData = await position.refreshData();
|
||||
|
@ -92,7 +92,7 @@ describe("position-impl", () => {
|
|||
const withdrawHalf = postIncreaseData.liquidity.div(new anchor.BN(2));
|
||||
const decrease_quote = await decreaseLiquidityQuoteByLiquidity(
|
||||
withdrawHalf,
|
||||
Percentage.fromFraction(1, 100),
|
||||
Percentage.fromFraction(0, 100),
|
||||
position,
|
||||
pool
|
||||
);
|
||||
|
@ -107,4 +107,110 @@ describe("position-impl", () => {
|
|||
);
|
||||
assert.equal(postWithdrawData.liquidity.toString(), expectedPostWithdrawLiquidity.toString());
|
||||
});
|
||||
|
||||
it("decrease liquidity on position with a different destination, position wallet", async () => {
|
||||
const { poolInitInfo } = await initTestPool(
|
||||
ctx,
|
||||
TickSpacing.Standard,
|
||||
PriceMath.priceToSqrtPriceX64(new Decimal(100), 6, 6)
|
||||
);
|
||||
|
||||
// Create and mint tokens in this wallet
|
||||
await mintTokensToTestAccount(
|
||||
ctx.provider,
|
||||
poolInitInfo.tokenMintA,
|
||||
10_500_000_000,
|
||||
poolInitInfo.tokenMintB,
|
||||
10_500_000_000
|
||||
);
|
||||
|
||||
const pool = await client.getPool(poolInitInfo.whirlpoolPda.publicKey);
|
||||
const lowerTick = PriceMath.priceToTickIndex(
|
||||
new Decimal(89),
|
||||
pool.getTokenAInfo().decimals,
|
||||
pool.getTokenBInfo().decimals
|
||||
);
|
||||
const upperTick = PriceMath.priceToTickIndex(
|
||||
new Decimal(120),
|
||||
pool.getTokenAInfo().decimals,
|
||||
pool.getTokenBInfo().decimals
|
||||
);
|
||||
|
||||
// [Action] Initialize Tick Arrays
|
||||
const initTickArrayTx = await pool.initTickArrayForTicks([lowerTick, upperTick]);
|
||||
await initTickArrayTx.buildAndExecute();
|
||||
|
||||
// [Action] Create a position at price 89, 120 with 50 token A
|
||||
const lowerPrice = new Decimal(89);
|
||||
const upperPrice = new Decimal(120);
|
||||
const { positionMint, positionAddress } = await initPosition(
|
||||
ctx,
|
||||
pool,
|
||||
lowerPrice,
|
||||
upperPrice,
|
||||
poolInitInfo.tokenMintA,
|
||||
50
|
||||
);
|
||||
|
||||
// [Action] Increase liquidity by 70 tokens of tokenB & create the ATA in the new source Wallet
|
||||
const position = await client.getPosition(positionAddress.publicKey);
|
||||
const preIncreaseData = position.getData();
|
||||
const increase_quote = increaseLiquidityQuoteByInputToken(
|
||||
poolInitInfo.tokenMintB,
|
||||
new Decimal(70),
|
||||
lowerTick,
|
||||
upperTick,
|
||||
Percentage.fromFraction(1, 100),
|
||||
pool
|
||||
);
|
||||
|
||||
await (
|
||||
await position.increaseLiquidity(increase_quote, ctx.wallet.publicKey)
|
||||
).buildAndExecute();
|
||||
|
||||
const postIncreaseData = await position.refreshData();
|
||||
const expectedPostIncreaseLiquidity = preIncreaseData.liquidity.add(
|
||||
increase_quote.liquidityAmount
|
||||
);
|
||||
assert.equal(postIncreaseData.liquidity.toString(), expectedPostIncreaseLiquidity.toString());
|
||||
|
||||
// [Action] Withdraw half of the liquidity away from the position and verify
|
||||
const withdrawHalf = postIncreaseData.liquidity.div(new anchor.BN(2));
|
||||
const decrease_quote = await decreaseLiquidityQuoteByLiquidity(
|
||||
withdrawHalf,
|
||||
Percentage.fromFraction(0, 100),
|
||||
position,
|
||||
pool
|
||||
);
|
||||
|
||||
// Transfer the position token to another wallet
|
||||
const otherWallet = anchor.web3.Keypair.generate();
|
||||
const walletPositionTokenAccount = await deriveATA(ctx.wallet.publicKey, positionMint);
|
||||
const newOwnerPositionTokenAccount = await createAssociatedTokenAccount(
|
||||
ctx.provider,
|
||||
positionMint,
|
||||
otherWallet.publicKey,
|
||||
ctx.wallet.publicKey
|
||||
);
|
||||
await transfer(provider, walletPositionTokenAccount, newOwnerPositionTokenAccount, 1);
|
||||
|
||||
// Withdraw liquidity into another wallet
|
||||
const destinationWallet = anchor.web3.Keypair.generate();
|
||||
await (
|
||||
await position.decreaseLiquidity(
|
||||
decrease_quote,
|
||||
destinationWallet.publicKey,
|
||||
otherWallet.publicKey,
|
||||
true
|
||||
)
|
||||
)
|
||||
.addSigner(otherWallet)
|
||||
.buildAndExecute();
|
||||
|
||||
const postWithdrawData = await position.refreshData();
|
||||
const expectedPostWithdrawLiquidity = postIncreaseData.liquidity.sub(
|
||||
decrease_quote.liquidityAmount
|
||||
);
|
||||
assert.equal(postWithdrawData.liquidity.toString(), expectedPostWithdrawLiquidity.toString());
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,17 +2,25 @@ import * as assert from "assert";
|
|||
import * as anchor from "@project-serum/anchor";
|
||||
import { WhirlpoolContext } from "../../../src/context";
|
||||
import { initTestPool } from "../../utils/init-utils";
|
||||
import { getTokenBalance, ONE_SOL, systemTransferTx, TickSpacing } from "../../utils";
|
||||
import {
|
||||
createAssociatedTokenAccount,
|
||||
getTokenBalance,
|
||||
ONE_SOL,
|
||||
systemTransferTx,
|
||||
TickSpacing,
|
||||
transfer,
|
||||
} from "../../utils";
|
||||
import {
|
||||
AccountFetcher,
|
||||
buildWhirlpoolClient,
|
||||
decreaseLiquidityQuoteByLiquidity,
|
||||
increaseLiquidityQuoteByInputToken,
|
||||
PDAUtil,
|
||||
PriceMath,
|
||||
TickUtil,
|
||||
} from "../../../src";
|
||||
import Decimal from "decimal.js";
|
||||
import { Percentage } from "@orca-so/common-sdk";
|
||||
import { deriveATA, Percentage } from "@orca-so/common-sdk";
|
||||
import { mintTokensToTestAccount } from "../../utils/test-builders";
|
||||
|
||||
describe("whirlpool-impl", () => {
|
||||
|
@ -89,7 +97,6 @@ describe("whirlpool-impl", () => {
|
|||
tickUpper,
|
||||
quote,
|
||||
ctx.wallet.publicKey,
|
||||
ctx.wallet.publicKey,
|
||||
funderKeypair.publicKey
|
||||
);
|
||||
|
||||
|
@ -127,4 +134,144 @@ describe("whirlpool-impl", () => {
|
|||
assert.equal(await getTokenBalance(ctx.provider, userTokenAAccount), mintedTokenAmount - 1);
|
||||
assert.equal(await getTokenBalance(ctx.provider, userTokenBAccount), mintedTokenAmount - 1);
|
||||
});
|
||||
|
||||
it("open and add liquidity to a position, transfer position to another wallet, then close the tokens to another wallet", async () => {
|
||||
const funderKeypair = anchor.web3.Keypair.generate();
|
||||
await systemTransferTx(provider, funderKeypair.publicKey, ONE_SOL).buildAndExecute();
|
||||
|
||||
const { poolInitInfo } = await initTestPool(
|
||||
ctx,
|
||||
TickSpacing.Standard,
|
||||
PriceMath.priceToSqrtPriceX64(new Decimal(100), 6, 6)
|
||||
);
|
||||
const pool = await client.getPool(poolInitInfo.whirlpoolPda.publicKey);
|
||||
|
||||
// Verify token mint info is correct
|
||||
const tokenAInfo = pool.getTokenAInfo();
|
||||
const tokenBInfo = pool.getTokenBInfo();
|
||||
assert.ok(tokenAInfo.mint.equals(poolInitInfo.tokenMintA));
|
||||
assert.ok(tokenBInfo.mint.equals(poolInitInfo.tokenMintB));
|
||||
|
||||
// Create and mint tokens in this wallet
|
||||
const mintedTokenAmount = 150_000_000;
|
||||
await mintTokensToTestAccount(
|
||||
ctx.provider,
|
||||
tokenAInfo.mint,
|
||||
mintedTokenAmount,
|
||||
tokenBInfo.mint,
|
||||
mintedTokenAmount
|
||||
);
|
||||
|
||||
// Open a position with no tick arrays initialized.
|
||||
const lowerPrice = new Decimal(96);
|
||||
const upperPrice = new Decimal(101);
|
||||
const poolData = pool.getData();
|
||||
const tokenADecimal = tokenAInfo.decimals;
|
||||
const tokenBDecimal = tokenBInfo.decimals;
|
||||
|
||||
const tickLower = TickUtil.getInitializableTickIndex(
|
||||
PriceMath.priceToTickIndex(lowerPrice, tokenADecimal, tokenBDecimal),
|
||||
poolData.tickSpacing
|
||||
);
|
||||
const tickUpper = TickUtil.getInitializableTickIndex(
|
||||
PriceMath.priceToTickIndex(upperPrice, tokenADecimal, tokenBDecimal),
|
||||
poolData.tickSpacing
|
||||
);
|
||||
|
||||
const inputTokenMint = poolData.tokenMintA;
|
||||
const depositAmount = new Decimal(50);
|
||||
const quote = increaseLiquidityQuoteByInputToken(
|
||||
inputTokenMint,
|
||||
depositAmount,
|
||||
tickLower,
|
||||
tickUpper,
|
||||
Percentage.fromFraction(1, 100),
|
||||
pool
|
||||
);
|
||||
|
||||
// [Action] Initialize Tick Arrays
|
||||
const initTickArrayTx = await pool.initTickArrayForTicks(
|
||||
[tickLower, tickUpper],
|
||||
funderKeypair.publicKey
|
||||
);
|
||||
await initTickArrayTx.addSigner(funderKeypair).buildAndExecute();
|
||||
|
||||
// [Action] Open Position (and increase L)
|
||||
const { positionMint, tx } = await pool.openPosition(
|
||||
tickLower,
|
||||
tickUpper,
|
||||
quote,
|
||||
ctx.wallet.publicKey,
|
||||
funderKeypair.publicKey
|
||||
);
|
||||
|
||||
await tx.addSigner(funderKeypair).buildAndExecute();
|
||||
|
||||
// Verify position exists and numbers fit input parameters
|
||||
const positionAddress = PDAUtil.getPosition(ctx.program.programId, positionMint).publicKey;
|
||||
const position = await client.getPosition(positionAddress);
|
||||
const positionData = position.getData();
|
||||
|
||||
const tickLowerIndex = TickUtil.getInitializableTickIndex(
|
||||
PriceMath.priceToTickIndex(lowerPrice, tokenAInfo.decimals, tokenBInfo.decimals),
|
||||
poolData.tickSpacing
|
||||
);
|
||||
const tickUpperIndex = TickUtil.getInitializableTickIndex(
|
||||
PriceMath.priceToTickIndex(upperPrice, tokenAInfo.decimals, tokenBInfo.decimals),
|
||||
poolData.tickSpacing
|
||||
);
|
||||
assert.ok(positionData.liquidity.eq(quote.liquidityAmount));
|
||||
assert.ok(positionData.tickLowerIndex === tickLowerIndex);
|
||||
assert.ok(positionData.tickUpperIndex === tickUpperIndex);
|
||||
assert.ok(positionData.positionMint.equals(positionMint));
|
||||
assert.ok(positionData.whirlpool.equals(poolInitInfo.whirlpoolPda.publicKey));
|
||||
|
||||
// Transfer the position token to another wallet
|
||||
const otherWallet = anchor.web3.Keypair.generate();
|
||||
const walletPositionTokenAccount = await deriveATA(ctx.wallet.publicKey, positionMint);
|
||||
const newOwnerPositionTokenAccount = await createAssociatedTokenAccount(
|
||||
ctx.provider,
|
||||
positionMint,
|
||||
otherWallet.publicKey,
|
||||
ctx.wallet.publicKey
|
||||
);
|
||||
await transfer(provider, walletPositionTokenAccount, newOwnerPositionTokenAccount, 1);
|
||||
|
||||
// [Action] Close Position
|
||||
const expectationQuote = await decreaseLiquidityQuoteByLiquidity(
|
||||
positionData.liquidity,
|
||||
Percentage.fromDecimal(new Decimal(0)),
|
||||
position,
|
||||
pool
|
||||
);
|
||||
|
||||
const destinationWallet = anchor.web3.Keypair.generate();
|
||||
await (
|
||||
await pool.closePosition(
|
||||
positionAddress,
|
||||
Percentage.fromFraction(1, 100),
|
||||
destinationWallet.publicKey,
|
||||
otherWallet.publicKey,
|
||||
ctx.wallet.publicKey
|
||||
)
|
||||
)
|
||||
.addSigner(otherWallet)
|
||||
.buildAndExecute();
|
||||
|
||||
// Verify position is closed and owner wallet has the tokens back
|
||||
const postClosePosition = await fetcher.getPosition(positionAddress, true);
|
||||
assert.ok(postClosePosition === null);
|
||||
|
||||
const dWalletTokenAAccount = await deriveATA(destinationWallet.publicKey, poolData.tokenMintA);
|
||||
const dWalletTokenBAccount = await deriveATA(destinationWallet.publicKey, poolData.tokenMintB);
|
||||
|
||||
assert.equal(
|
||||
await getTokenBalance(ctx.provider, dWalletTokenAAccount),
|
||||
expectationQuote.tokenMinA.toString()
|
||||
);
|
||||
assert.equal(
|
||||
await getTokenBalance(ctx.provider, dWalletTokenBAccount),
|
||||
expectationQuote.tokenMinB.toString()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -178,17 +178,20 @@ export async function mintTokensToTestAccount(
|
|||
tokenAMint: PublicKey,
|
||||
tokenMintForA: number,
|
||||
tokenBMint: PublicKey,
|
||||
tokenMintForB: number
|
||||
tokenMintForB: number,
|
||||
destinationWallet?: PublicKey
|
||||
) {
|
||||
const userTokenAAccount = await createAndMintToAssociatedTokenAccount(
|
||||
provider,
|
||||
tokenAMint,
|
||||
tokenMintForA
|
||||
tokenMintForA,
|
||||
destinationWallet
|
||||
);
|
||||
const userTokenBAccount = await createAndMintToAssociatedTokenAccount(
|
||||
provider,
|
||||
tokenBMint,
|
||||
tokenMintForB
|
||||
tokenMintForB,
|
||||
destinationWallet
|
||||
);
|
||||
|
||||
return [userTokenAAccount, userTokenBAccount];
|
||||
|
@ -200,8 +203,10 @@ export async function initPosition(
|
|||
lowerPrice: Decimal,
|
||||
upperPrice: Decimal,
|
||||
inputTokenMint: PublicKey,
|
||||
inputTokenAmount: number
|
||||
inputTokenAmount: number,
|
||||
sourceWallet?: Keypair
|
||||
) {
|
||||
const sourceWalletKey = sourceWallet ? sourceWallet.publicKey : ctx.wallet.publicKey;
|
||||
const tokenADecimal = pool.getTokenAInfo().decimals;
|
||||
const tokenBDecimal = pool.getTokenBInfo().decimals;
|
||||
const tickSpacing = pool.getData().tickSpacing;
|
||||
|
@ -231,10 +236,15 @@ export async function initPosition(
|
|||
lowerTick,
|
||||
upperTick,
|
||||
quote,
|
||||
ctx.wallet.publicKey,
|
||||
sourceWalletKey,
|
||||
sourceWalletKey,
|
||||
ctx.wallet.publicKey
|
||||
);
|
||||
|
||||
if (sourceWallet) {
|
||||
tx.addSigner(sourceWallet);
|
||||
}
|
||||
|
||||
await tx.buildAndExecute();
|
||||
|
||||
return {
|
||||
|
|
|
@ -60,7 +60,8 @@ export async function createTokenAccount(
|
|||
export async function createAssociatedTokenAccount(
|
||||
provider: Provider,
|
||||
mint: web3.PublicKey,
|
||||
owner: web3.PublicKey
|
||||
owner: web3.PublicKey,
|
||||
payer: web3.PublicKey
|
||||
) {
|
||||
const ataAddress = await deriveATA(owner, mint);
|
||||
|
||||
|
@ -70,7 +71,7 @@ export async function createAssociatedTokenAccount(
|
|||
mint,
|
||||
ataAddress,
|
||||
owner,
|
||||
owner
|
||||
payer
|
||||
);
|
||||
const tx = new web3.Transaction();
|
||||
tx.add(instr);
|
||||
|
@ -147,12 +148,17 @@ export async function createAndMintToTokenAccount(
|
|||
export async function createAndMintToAssociatedTokenAccount(
|
||||
provider: Provider,
|
||||
mint: web3.PublicKey,
|
||||
amount: number | BN
|
||||
amount: number | BN,
|
||||
destinationWallet?: web3.PublicKey,
|
||||
payer?: web3.PublicKey
|
||||
): Promise<web3.PublicKey> {
|
||||
const destinationWalletKey = destinationWallet ? destinationWallet : provider.wallet.publicKey;
|
||||
const payerKey = payer ? payer : provider.wallet.publicKey;
|
||||
const tokenAccount = await createAssociatedTokenAccount(
|
||||
provider,
|
||||
mint,
|
||||
provider.wallet.publicKey
|
||||
destinationWalletKey,
|
||||
payerKey
|
||||
);
|
||||
await mintToByAuthority(provider, mint, tokenAccount, amount);
|
||||
return tokenAccount;
|
||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -565,13 +565,13 @@
|
|||
"@nodelib/fs.scandir" "2.1.5"
|
||||
fastq "^1.6.0"
|
||||
|
||||
"@orca-so/common-sdk@^0.0.4":
|
||||
version "0.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@orca-so/common-sdk/-/common-sdk-0.0.4.tgz#cbe135ed2026ce98c5771ff9e3de5e24ec90cf8d"
|
||||
integrity sha512-3YrVgrgt2HlDyzwE6KwmD/u3knFjNCGtKi/1HJsBQMcR95R99vVnsvQylnUcZ6CJm1KyjTAOl5lvEIzgFnu+3g==
|
||||
"@orca-so/common-sdk@^0.0.6":
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@orca-so/common-sdk/-/common-sdk-0.0.6.tgz#531075be74be2dbb8ef6c302af8685d476133df0"
|
||||
integrity sha512-MSpOwmq7llLipJ4tYcT4L/7rzhqioWaYflox0lV3tlo7agv6EEm9UD8rkl0sbt5p3oe+zjnWOq/6JkCUc1GQhg==
|
||||
dependencies:
|
||||
"@project-serum/anchor" "0.20.1"
|
||||
"@solana/spl-token" "^0.1.8"
|
||||
"@solana/spl-token" "0.1.8"
|
||||
decimal.js "^10.3.1"
|
||||
|
||||
"@orca-so/whirlpool-client-sdk@0.0.7":
|
||||
|
@ -633,7 +633,7 @@
|
|||
dependencies:
|
||||
buffer "~6.0.3"
|
||||
|
||||
"@solana/spl-token@^0.1.8":
|
||||
"@solana/spl-token@0.1.8", "@solana/spl-token@^0.1.8":
|
||||
version "0.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.1.8.tgz#f06e746341ef8d04165e21fc7f555492a2a0faa6"
|
||||
integrity sha512-LZmYCKcPQDtJgecvWOgT/cnoIQPWjdH+QVyzPcFvyDUiT0DiRjZaam4aqNUyvchLFhzgunv3d9xOoyE34ofdoQ==
|
||||
|
|
Loading…
Reference in New Issue