-
Notifications
You must be signed in to change notification settings - Fork 166
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
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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, | ||
}); | ||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)); | ||
}); | ||
}); |
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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
), | ||
); | ||
}); | ||
}); |
There was a problem hiding this comment.
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?