Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unit tests for decreaseLiquidity.ts #545

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions ts-sdk/whirlpool/tests/closePosition.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { describe, it, beforeAll } from "vitest";
import type { Address } from "@solana/web3.js";
import { address, assertAccountExists } from "@solana/web3.js";
import { setupAta, setupMint } from "./utils/token";
import {
setupAtaTE,
setupMintTE,
setupMintTEFee,
} from "./utils/tokenExtensions";
import {
setupWhirlpool,
setupPosition,
setupTEPosition,
} from "./utils/program";
import { closePositionInstructions } from "../src/decreaseLiquidity";
import { rpc, sendTransaction } from "./utils/mockRpc";
import {
fetchMaybePosition,
getPositionAddress,
} from "@orca-so/whirlpools-client";
import assert from "assert";

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],
]);

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

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) {
const position = await setupPosition(poolAddress, {
tickLower: -100,
tickUpper: 100,
liquidity,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One test to close a postion without liquidity?

});
positions.set(poolName, position);
const positionTE = await setupTEPosition(poolAddress, {
tickLower: -100,
tickUpper: 100,
liquidity,
});
positions.set(`${poolName} (TE position)`, positionTE);
}
});

const testClosePositionInstructions = async (poolName: string) => {
const positionMint = positions.get(poolName)!;
const positionAddress = await getPositionAddress(positionMint);
const positionBefore = await fetchMaybePosition(rpc, positionAddress[0]);

assertAccountExists(positionBefore);

const { instructions } = await closePositionInstructions(rpc, positionMint);

await sendTransaction(instructions);

const positionAfter = await fetchMaybePosition(rpc, positionAddress[0]);
assert.strictEqual(positionAfter.exists, false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also check if fees/rewards are harvested correctly?

};

for (const poolName of poolTypes.keys()) {
it(`Should close the position for ${poolName}`, async () => {
await testClosePositionInstructions(poolName);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TE Position?

});
}

it("Should throw an error if the position mint can not be found", async () => {
const positionMint: Address = address(
"123456789abcdefghijkmnopqrstuvwxABCDEFGHJKL",
);
await assert.rejects(closePositionInstructions(rpc, positionMint));
});
});
166 changes: 163 additions & 3 deletions ts-sdk/whirlpool/tests/decreaseLiquidity.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,165 @@
import { describe } from "vitest";
import { describe, it, beforeAll } from "vitest";
import { decreaseLiquidityInstructions } from "../src/decreaseLiquidity";
import { rpc, signer, sendTransaction } from "./utils/mockRpc";
import { setupMint, setupAta } from "./utils/token";
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 {
setupPosition,
setupTEPosition,
setupWhirlpool,
} from "./utils/program";
import {
setupAtaTE,
setupMintTE,
setupMintTEFee,
} from "./utils/tokenExtensions";
import { DEFAULT_FUNDER, setDefaultFunder } from "../src/config";

describe.skip("Decrease Liquidity", () => {
// TODO: <-
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 Instructions", () => {
const tickSpacing = 64;
const tokenBalance = 1_000_000n;
const liquidity = 100_000n;
const atas: Map<string, Address> = new Map();
const positions: Map<string, Address> = new Map();

beforeAll(async () => {
const mints: Map<string, Address> = new Map();
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 }));
}

const pools: Map<string, Address> = new Map();
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,
});
positions.set(`${poolName} ${positionTypeName}`, position);

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

const testDecreaseLiquidity = async (
positionName: string,
poolName: string,
) => {
const positionMint = positions.get(positionName)!;
const [mintAKey, mintBKey] = poolName.split("-");
const ataA = atas.get(mintAKey)!;
const ataB = atas.get(mintBKey)!;
const param = { liquidity: 10_000n };

const { quote, instructions } = await decreaseLiquidityInstructions(
rpc,
positionMint,
param,
);

const tokenBeforeA = await fetchToken(rpc, ataA);
const tokenBeforeB = await fetchToken(rpc, ataB);
await sendTransaction(instructions);
const positionAddress = await getPositionAddress(positionMint);
const position = await fetchPosition(rpc, positionAddress[0]);
const tokenAfterA = await fetchToken(rpc, ataA);
const tokenAfterB = await fetchToken(rpc, ataB);
const balanceChangeTokenA =
tokenAfterA.data.amount - tokenBeforeA.data.amount;
const balanceChangeTokenB =
tokenAfterB.data.amount - tokenBeforeB.data.amount;

assert.strictEqual(quote.tokenEstA, balanceChangeTokenA);
assert.strictEqual(quote.tokenEstB, balanceChangeTokenB);
assert.strictEqual(
liquidity - quote.liquidityDelta,
position.data.liquidity,
);
};

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

it("Should throw error if authority is default address", async () => {
const param = { liquidity: liquidity / 2n };
setDefaultFunder(DEFAULT_FUNDER);
await assert.rejects(
decreaseLiquidityInstructions(
rpc,
positions.entries().next().value,
param,
),
);
setDefaultFunder(signer);
});

it("Should throw error when decrease liquidity amount exceeds position liquidity", async () => {
const param = { liquidity: liquidity * 2n };
await assert.rejects(
decreaseLiquidityInstructions(
rpc,
positions.entries().next().value,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Maybe hard-code the key here so it is clear which position it is getting

param,
),
);
});
});
Loading