[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:
meep 2022-06-08 19:36:48 -07:00 committed by GitHub
parent efe85aa44d
commit f888d1cf20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 418 additions and 116 deletions

View File

@ -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",

View File

@ -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() {

View File

@ -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,

View File

@ -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

View File

@ -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());
});
});

View File

@ -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()
);
});
});

View File

@ -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 {

View File

@ -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;

View File

@ -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==