Skip to content

Commit

Permalink
Add tests for close position, decrease liquidity, and harvest position (
Browse files Browse the repository at this point in the history
#653)

* Add tests for close position and decrease liquidity

* Add tests for harvest position

* Add testHarvestPositionInstructionsWithoutSwaps()

* Some modifications

* Fix to generate some fees before testing
  • Loading branch information
pauldragonfly authored Jan 19, 2025
1 parent 4a79e52 commit 06d3c68
Show file tree
Hide file tree
Showing 4 changed files with 689 additions and 14 deletions.
211 changes: 211 additions & 0 deletions ts-sdk/whirlpool/tests/closePosition.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import {
fetchMaybePosition,
getPositionAddress,
} from "@orca-so/whirlpools-client";
import { fetchToken } from "@solana-program/token";
import type { Address } from "@solana/web3.js";
import { address } from "@solana/web3.js";
import assert from "assert";
import { beforeAll, describe, it } from "vitest";
import { closePositionInstructions } from "../src/decreaseLiquidity";
import { swapInstructions } from "../src/swap";
import { rpc, sendTransaction } from "./utils/mockRpc";
import {
setupPosition,
setupTEPosition,
setupWhirlpool,
} from "./utils/program";
import { setupAta, setupMint } from "./utils/token";
import {
setupAtaTE,
setupMintTE,
setupMintTEFee,
} from "./utils/tokenExtensions";

const mintTypes = new Map([
["A", setupMint],
["B", setupMint],
["TEA", setupMintTE],
["TEB", setupMintTE],
["TEFee", setupMintTEFee],
]);

const ataTypes = new Map([
["A", setupAta],
["B", setupAta],
["TEA", setupAtaTE],
["TEB", setupAtaTE],
["TEFee", setupAtaTE],
]);

const poolTypes = new Map([
["A-B", setupWhirlpool],
["A-TEA", setupWhirlpool],
["TEA-TEB", setupWhirlpool],
["A-TEFee", setupWhirlpool],
]);

const positionTypes = new Map([
["equally centered", { tickLower: -100, tickUpper: 100 }],
["one sided A", { tickLower: -100, tickUpper: -1 }],
["one sided B", { tickLower: 1, tickUpper: 100 }],
]);

describe("Close Position", () => {
const atas: Map<string, Address> = new Map();
const initialLiquidity = 100_000n;
const mints: Map<string, Address> = new Map();
const pools: Map<string, Address> = new Map();
const positions: Map<string, Address> = new Map();
const tickSpacing = 64;
const tokenBalance = 1_000_000n;

beforeAll(async () => {
for (const [name, setup] of mintTypes) {
mints.set(name, await setup());
}

for (const [name, setup] of ataTypes) {
const mint = mints.get(name)!;
atas.set(name, await setup(mint, { amount: tokenBalance }));
}

for (const [name, setup] of poolTypes) {
const [mintAKey, mintBKey] = name.split("-");
const mintA = mints.get(mintAKey)!;
const mintB = mints.get(mintBKey)!;
pools.set(name, await setup(mintA, mintB, tickSpacing));
}

for (const [poolName, poolAddress] of pools) {
for (const [positionTypeName, tickRange] of positionTypes) {
const position = await setupPosition(poolAddress, {
...tickRange,
liquidity: initialLiquidity,
});
positions.set(`${poolName} ${positionTypeName}`, position);

const positionTE = await setupTEPosition(poolAddress, {
...tickRange,
liquidity: initialLiquidity,
});
positions.set(`TE ${poolName} ${positionTypeName}`, positionTE);
}
}

for (const [poolName, poolAddress] of pools) {
const [mintAName, mintBName] = poolName.split("-");
const mintAAddress = mints.get(mintAName)!;
const mintBAddress = mints.get(mintBName)!;

let { instructions: swap_instructions } = await swapInstructions(
rpc,
{ inputAmount: 100n, mint: mintAAddress },
poolAddress,
);
await sendTransaction(swap_instructions);

// Do another swap to generate more fees
({ instructions: swap_instructions } = await swapInstructions(
rpc,
{ outputAmount: 100n, mint: mintAAddress },
poolAddress,
));
await sendTransaction(swap_instructions);

// Do another swap to generate more fees
({ instructions: swap_instructions } = await swapInstructions(
rpc,
{ inputAmount: 100n, mint: mintBAddress },
poolAddress,
));
await sendTransaction(swap_instructions);

// Do another swap to generate more fees
({ instructions: swap_instructions } = await swapInstructions(
rpc,
{ outputAmount: 100n, mint: mintBAddress },
poolAddress,
));
await sendTransaction(swap_instructions);
}
});

const testClosePositionInstructions = async (
poolName: string,
positionName: string,
) => {
const [mintAName, mintBName] = poolName.split("-");
const ataAAddress = atas.get(mintAName)!;
const ataBAddress = atas.get(mintBName)!;

const positionMintAddress = positions.get(positionName)!;
const [positionAddress, _] = await getPositionAddress(positionMintAddress);

const tokenABefore = await fetchToken(rpc, ataAAddress);
const tokenBBefore = await fetchToken(rpc, ataBAddress);

const { instructions, quote, feesQuote } = await closePositionInstructions(
rpc,
positionMintAddress,
);
await sendTransaction(instructions);

const positionAfter = await fetchMaybePosition(rpc, positionAddress);
const tokenAAfter = await fetchToken(rpc, ataAAddress);
const tokenBAfter = await fetchToken(rpc, ataBAddress);

assert.strictEqual(positionAfter.exists, false);

assert.strictEqual(
quote.tokenEstA + feesQuote.feeOwedA,
tokenAAfter.data.amount - tokenABefore.data.amount,
);

assert.strictEqual(
quote.tokenEstB + feesQuote.feeOwedB,
tokenBAfter.data.amount - tokenBBefore.data.amount,
);
};

for (const poolName of poolTypes.keys()) {
for (const positionTypeName of positionTypes.keys()) {
const positionName = `${poolName} ${positionTypeName}`;
it(`Should close a position for ${positionName}`, async () => {
await testClosePositionInstructions(poolName, positionName);
});

const positionNameTE = `TE ${poolName} ${positionTypeName}`;
it(`Should close a position for ${positionNameTE}`, async () => {
await testClosePositionInstructions(poolName, positionNameTE);
});
}
}

it("Should close a position without liquidity", async () => {
const poolName = "A-B";
const pool = pools.get(poolName)!;
const positionName = "A-B with 0 liquidity";

positions.set(
positionName,
await setupPosition(pool, {
tickLower: -100,
tickUpper: 100,
liquidity: 0n,
}),
);

await assert.doesNotReject(
testClosePositionInstructions(poolName, positionName),
);
});

it("Should throw an error if the position mint can not be found", async () => {
const positionMintAddress: Address = address(
"123456789abcdefghijkmnopqrstuvwxABCDEFGHJKL",
);

await assert.rejects(closePositionInstructions(rpc, positionMintAddress));
});
});
168 changes: 165 additions & 3 deletions ts-sdk/whirlpool/tests/decreaseLiquidity.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,167 @@
import { describe } from "vitest";
import { fetchPosition, getPositionAddress } from "@orca-so/whirlpools-client";
import { fetchToken } from "@solana-program/token-2022";
import type { Address } from "@solana/web3.js";
import assert from "assert";
import { beforeAll, describe, it } from "vitest";
import { DEFAULT_FUNDER, setDefaultFunder } from "../src/config";
import { decreaseLiquidityInstructions } from "../src/decreaseLiquidity";
import { rpc, sendTransaction, signer } from "./utils/mockRpc";
import {
setupPosition,
setupTEPosition,
setupWhirlpool,
} from "./utils/program";
import { setupAta, setupMint } from "./utils/token";

describe.skip("Decrease Liquidity", () => {
// TODO: <-
import {
setupAtaTE,
setupMintTE,
setupMintTEFee,
} from "./utils/tokenExtensions";

const mintTypes = new Map([
["A", setupMint],
["B", setupMint],
["TEA", setupMintTE],
["TEB", setupMintTE],
["TEFee", setupMintTEFee],
]);

const ataTypes = new Map([
["A", setupAta],
["B", setupAta],
["TEA", setupAtaTE],
["TEB", setupAtaTE],
["TEFee", setupAtaTE],
]);

const poolTypes = new Map([
["A-B", setupWhirlpool],
["A-TEA", setupWhirlpool],
["TEA-TEB", setupWhirlpool],
["A-TEFee", setupWhirlpool],
]);

const positionTypes = new Map([
["equally centered", { tickLower: -100, tickUpper: 100 }],
["one sided A", { tickLower: -100, tickUpper: -1 }],
["one sided B", { tickLower: 1, tickUpper: 100 }],
]);

describe("Decrease Liquidity", () => {
const atas: Map<string, Address> = new Map();
const initialLiquidity = 100_000n;
const mints: Map<string, Address> = new Map();
const pools: Map<string, Address> = new Map();
const positions: Map<string, Address> = new Map();
const tickSpacing = 64;
const tokenBalance = 1_000_000n;

beforeAll(async () => {
for (const [name, setup] of mintTypes) {
mints.set(name, await setup());
}

for (const [name, setup] of ataTypes) {
const mint = mints.get(name)!;
atas.set(name, await setup(mint, { amount: tokenBalance }));
}

for (const [name, setup] of poolTypes) {
const [mintAKey, mintBKey] = name.split("-");
const mintA = mints.get(mintAKey)!;
const mintB = mints.get(mintBKey)!;
pools.set(name, await setup(mintA, mintB, tickSpacing));
}

for (const [poolName, poolAddress] of pools) {
for (const [positionTypeName, tickRange] of positionTypes) {
const position = await setupPosition(poolAddress, {
...tickRange,
liquidity: initialLiquidity,
});
positions.set(`${poolName} ${positionTypeName}`, position);

const positionTE = await setupTEPosition(poolAddress, {
...tickRange,
liquidity: initialLiquidity,
});
positions.set(`TE ${poolName} ${positionTypeName}`, positionTE);
}
}
});

const testDecreaseLiquidity = async (
poolName: string,
positionName: string,
) => {
const [mintAName, mintBName] = poolName.split("-");
const ataAAddress = atas.get(mintAName)!;
const ataBAddress = atas.get(mintBName)!;
const liquidityToDecrease = 10_000n;
const positionMintAddress = positions.get(positionName)!;

const { quote, instructions } = await decreaseLiquidityInstructions(
rpc,
positionMintAddress,
{
liquidity: liquidityToDecrease,
},
);

const tokenBeforeA = await fetchToken(rpc, ataAAddress);
const tokenBeforeB = await fetchToken(rpc, ataBAddress);

await sendTransaction(instructions);

const tokenAfterA = await fetchToken(rpc, ataAAddress);
const tokenAfterB = await fetchToken(rpc, ataBAddress);

assert.strictEqual(
quote.tokenEstA,
tokenAfterA.data.amount - tokenBeforeA.data.amount,
);
assert.strictEqual(
quote.tokenEstB,
tokenAfterB.data.amount - tokenBeforeB.data.amount,
);

const positionAddress = await getPositionAddress(positionMintAddress);
const position = await fetchPosition(rpc, positionAddress[0]);

assert.strictEqual(
initialLiquidity - quote.liquidityDelta,
position.data.liquidity,
);
};

for (const poolName of poolTypes.keys()) {
for (const positionTypeName of positionTypes.keys()) {
const positionName = `${poolName} ${positionTypeName}`;
it(`Should decrease liquidity for ${positionName}`, async () => {
await testDecreaseLiquidity(poolName, positionName);
});

const positionNameTE = `TE ${poolName} ${positionTypeName}`;
it(`Should decrease liquidity for ${positionNameTE}`, async () => {
await testDecreaseLiquidity(poolName, positionNameTE);
});
}
}

it("Should throw an error if the signer is not valid", async () => {
const liquidityToDecrease = 10_000n;

setDefaultFunder(DEFAULT_FUNDER);

await assert.rejects(
decreaseLiquidityInstructions(
rpc,
positions.get("A-B equally centered")!,
{ liquidity: liquidityToDecrease },
),
);

setDefaultFunder(signer);
});
});
Loading

0 comments on commit 06d3c68

Please sign in to comment.