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

Add unit tests for createPool.ts #501

Merged
merged 22 commits into from
Nov 15, 2024
Merged

Add unit tests for createPool.ts #501

merged 22 commits into from
Nov 15, 2024

Conversation

calintje
Copy link
Contributor

Title
Add unit tests for createPool.ts

Details

  • Fixed the calculation of estInitializationCosts in the createConcentratedLiquidityPool function.
  • Added tests to verify the correct calculation of estInitialization costs.
  • Updated setupWhirlpool to support the V2 instruction of our program.

.send();
const nonRefundableRents: Lamports[] = await Promise.all(
stateSpaces.map(async (space) => {
const rentExemption = await rpc.getMinimumBalanceForRentExemption(BigInt(space)).send();
Copy link
Member

Choose a reason for hiding this comment

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

I wouldn't do this since it would change the rpc calls it needs to make from 1 to 5 (or 6)

stateSpace += getWhirlpoolSize();
stateSpaces.push(
tokenProgramA === TOKEN_2022_PROGRAM_ADDRESS
? getTokenSize(getAccountExtensions(mintA.data))
Copy link
Member

Choose a reason for hiding this comment

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

Can we make this a separate function (should be able to check mint.owner so mint would be the only parameter)?

Copy link
Member

Choose a reason for hiding this comment

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

Suspect we will need the same thing in increase_liquidity too. We can put it in token.ts

Copy link
Member

Choose a reason for hiding this comment

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

And then we can write a test for it 😄

setDefaultFunder(signer);
});

it("Should throw an error if token mints are not ordered", async () => {
Copy link
Member

Choose a reason for hiding this comment

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

nit: they are ordered, just not ordered correctly

await assert.rejects(
createConcentratedLiquidityPoolInstructions(rpc, mintB, mintA, 64, 1),
{
name: 'AssertionError',
Copy link
Member

Choose a reason for hiding this comment

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

Not sure if it exists but is there a new AssertionError("Token order needs to be flipped to match the canonical ordering (i.e. sorted on the byte repr. of the mint pubkeys)")

createConcentratedLiquidityPoolInstructions(rpc, mintB, mintA, 64, 1),
{
name: 'AssertionError',
message: 'Token order needs to be flipped to match the canonical ordering (i.e. sorted on the byte repr. of the mint pubkeys)'
Copy link
Member

Choose a reason for hiding this comment

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

nit: we use double quotes for strings in ts (but you can run yarn format and that should format your files correctly

await createSplashPoolInstructions(rpc, mintA, mintB, price);
await sendTransaction(instructions);

const pool = await fetchWhirlpool(rpc, poolAddress);
Copy link
Member

Choose a reason for hiding this comment

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

I would structure the test like this:

  • create splash pool
  • fetch maybe pool before
  • fetch signer balance before
  • assert pool does not exist
  • send tx
  • fetch maybe pool
  • fetch signer blance after
  • assert pool exists
  • assert sqrt, mintA, mintB, and tick spacing
  • assert that estInitialization cost equals difference in signer balance

That way you don't need the Should estimate initialization costs correctly test case entirely

Copy link
Member

Choose a reason for hiding this comment

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

And then create more tests with similar structure for:

  • Splash pool with TE token
  • CL pool
  • CL pool with TE token
  • etc.

import {
SPLASH_POOL_TICK_SPACING,
WHIRLPOOLS_CONFIG_ADDRESS,
} from "../../src/config";
import { tickIndexToSqrtPrice } from "@orca-so/whirlpools-core";
import { fetchMint } from "@solana-program/token";
import { a } from "vitest/dist/chunks/suite.B2jumIFP.js";
Copy link
Member

Choose a reason for hiding this comment

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

?

.send();
const nonRefundableRents: Lamports[] = await Promise.all(
stateSpaces.map(async (space) => {
const rentExemption = await calculateMinimumBalance(rpc, space);
Copy link
Member

Choose a reason for hiding this comment

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

This is still doing the same thing where it fetches the rent 5 (or 6 times).

The way I would do it. Change the function to:

export function calculateMinimumBalance(
  rent: Rent,
  dataSize: number,
): Lamports {

Fetcht he rent sysvar at the beginning of this function and then instead of keeping track of stateSpace: Array<bigint> you can just straight up keep track of estInitializationCost: Lamports.

And then instead of doing stateSpace.push(...) you can just do estInitializationCost += calculateMinimumBalance(rent, ...)

* @param {number} dataSize - The size of the account data in bytes.
* @returns {Promise<BigInt>} The minimum balance required for rent exemption in lamports.
*/
export async function calculateMinimumBalance(
Copy link
Member

Choose a reason for hiding this comment

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

Can you write a test for this?

const price = 10;
const sqrtPrice = priceToSqrtPrice(price, 6, 6);

let signerAccount = await rpc.getAccountInfo(signer.address).send();
Copy link
Member

Choose a reason for hiding this comment

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

Think nicer if we implement getBalance on the mockRpc so we don't have to duplicate this every time.

assertAccountExists(pool);

signerAccount = await rpc.getAccountInfo(signer.address).send();
const balanceAfter = signerAccount.value?.lamports ?? lamports(0n);
Copy link
Member

Choose a reason for hiding this comment

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

nit: I would group all the fetch together before starting on any assertions. poolBefore together with balanceBefore, poolAfter together with balanceAfter.

const balanceChange = balanceBefore - balanceAfter;
const txFee = 15000n; // 3 signing accounts * 5000 lamports
const minRentExempt = balanceChange - txFee;
assert.strictEqual(estInitializationCost, minRentExempt);
Copy link
Member

Choose a reason for hiding this comment

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

Let me see if I can make the tx fee go to 0 in bankrun so that this assertion can just become:

assert.strictEqual(estInitializationCost, balanceBefore - balanceAfter);

});

it("Should create splash pool with 1 TE token", async () => {
const mint1 = await setupMint();
Copy link
Member

Choose a reason for hiding this comment

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

I would set up these mints in the beforeAll as well since that will get faster execution (init the TE mint once opposed to doing it for each TE test)

});

it("Should create splash pool with 2 TE tokens", async () => {
const mint1 = await setupMintTEFee();
Copy link
Member

Choose a reason for hiding this comment

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

For the tests I would do (instead of trying to combine Transfer fee and 1/2 TE token tests):

  • 1 TE token (without extensions)
  • 2 TE tokens (without extensions)
  • 1 TE token (with transfer fee)

@@ -0,0 +1,28 @@
import { fetchSysvarRent } from "@solana/sysvars";
Copy link
Member

Choose a reason for hiding this comment

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

nit: This should be re-exported by @solana/web3.js so you can use that as import

@calintje calintje marked this pull request as draft November 15, 2024 02:46
@calintje calintje marked this pull request as ready for review November 15, 2024 03:18
*/
export async function calculateMinimumBalance(
rpc: Rpc<GetAccountInfoApi>,
export function calculateMinimumBalance(
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 we can be a bit more descriptive with this naming. getMinimumBalanceForRentExemption is what the rpc method that does the same is called

mintTE1 = await setupMintTE();
mintTE2 = await setupMintTE();
mintTEFee1 = await setupMintTEFee();
mintTEFee2 = await setupMintTEFee();
Copy link
Member

Choose a reason for hiding this comment

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

Do you really need to have this second TEFee mint?

let rent: SysvarRent;

beforeAll(async () => {
rent = {
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't necessarily have to be in the beforeAll (since it is not asynchronous). You can just init the rent directly as a const

});

it("Should calculate the correct minimum balance for a token account", async () => {
const mint = await setupMint();
Copy link
Member

Choose a reason for hiding this comment

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

You can make this test synchronous since you don't need to store/fetch anything in bankrun

@@ -103,7 +103,19 @@ const decoders: Record<string, VariableSizeDecoder<string>> = {
base64: getBase64Decoder(),
};

async function getAccountData<T>(address: unknown, opts: unknown): Promise<T> {
interface AccountData {
Copy link
Member

Choose a reason for hiding this comment

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

Not 100% sure if the AccountData is always exactly in this format. IIRC data depends on the encoding specified and is not always [string, string]. lamports/rentEpoch might be numbers I think (hence why I put generic T).

Instead of adding this interface you could also just do an assertion in getBalance to check if lamports exists in the object and then return that:

    const accountDataForBalance = await getAccountData(
        addressForBalance,
        config.payload.params[1],
      );
      assert("lamports" in accountDataForBalance);
      return getResponseWithContext<T>(accountDataForBalance.lamports);

await createSplashPoolInstructions(rpc, mintA, mintB, price);

const balanceBefore = await rpc.getBalance(signer.address).send();
const maybePool = await fetchMaybeWhirlpool(rpc, poolAddress);
Copy link
Member

Choose a reason for hiding this comment

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

nit: I would name them poolBefore and poolAfter

@@ -33,6 +32,8 @@ import {
getCreateAccountWithSeedInstruction,
getTransferSolInstruction,
} from "@solana-program/system";
import { getTokenSize } from "@solana-program/token";
import { getTokenSize as getToken22Size } from "@solana-program/token-2022";
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 getTokenWithExtensionsSize since if it is a t22 token with no extensions we still the other one

const txFee = 15000n; // 3 signing accounts * 5000 lamports
const minRentExempt = balanceChange - txFee;

assertAccountExists(pool);
Copy link
Member

Choose a reason for hiding this comment

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

Interesting! Never knew this existed

assert.strictEqual(SPLASH_POOL_TICK_SPACING, pool.data.tickSpacing);
});

it("Should create splash pool with 1 TE tokens (with extension)", async () => {
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 instead of with extension be explicit about that it is the TE extension

tokenSize,
);

const expectedMinimumBalance = lamports((165n + OVERHEAD) * 10n);
Copy link
Member

@wjthieme wjthieme Nov 15, 2024

Choose a reason for hiding this comment

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

I wouldn't do the calculation here and just assert agains the actual value. OVERHEAD is something internal of the function and shouldn't be here. From a test you need to assume that you know nothing about the internal workings of the function and only test inputs and outputs

Copy link
Member

Choose a reason for hiding this comment

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

So just 1445n

assert.strictEqual(tokenSize, 165);
});

it("Should get the correct token size for TOKEN_2022_PROGRAM mint with", async () => {
Copy link
Member

Choose a reason for hiding this comment

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

nit: mint with ...

@calintje calintje merged commit 3ba9c08 into main Nov 15, 2024
5 checks passed
@calintje calintje deleted the calintje/ts-sdk-unit-tests branch November 15, 2024 20:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants