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 all 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
2 changes: 1 addition & 1 deletion ts-sdk/whirlpool/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
],
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts --sourcemap",
"test": "vitest run tests",
"test": "vitest run tests/closePosition.test.ts",
"clean": "rimraf dist"
},
"dependencies": {
Expand Down
131 changes: 131 additions & 0 deletions ts-sdk/whirlpool/tests/closePosition.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
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(`position for ${poolName}`, position);
const positionTE = await setupTEPosition(poolAddress, {
tickLower: -100,
tickUpper: 100,
liquidity,
});
positions.set(`TE position for ${poolName}`, positionTE);
}
});

const testClosePositionInstructions = async (positionName: string) => {
const positionMint = positions.get(positionName)!;
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()) {
const positioName = `position for ${poolName}`;
it(`Should close the ${positioName}`, async () => {
await testClosePositionInstructions(positioName);
});
const tePositionName = `TE position for ${poolName}`;
it(`Should close the ${tePositionName}`, async () => {
await testClosePositionInstructions(tePositionName);
});
}

it("Should close a position without liquidity", async () => {
const pool = pools.get("A-B")!;
const positionName = `position for A-B with 0 liquidity`;
positions.set(positionName, await setupPosition(pool, {
tickLower: -100,
tickUpper: 100,
liquidity: 0n,
}));
await testClosePositionInstructions(positionName);
});

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));
});
});
155 changes: 152 additions & 3 deletions ts-sdk/whirlpool/tests/decreaseLiquidity.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,154 @@
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.get("A-B equally centered")!,
param,
),
);
setDefaultFunder(signer);
});
});
Loading