|
| 1 | +import { describe, it, beforeAll } from "vitest"; |
| 2 | +import type { Address } from "@solana/kit"; |
| 3 | +import { rpc, sendTransaction, signer } from "./utils/mockRpc"; |
| 4 | +import assert from "assert"; |
| 5 | +import { setupAta, setupMint } from "./utils/token"; |
| 6 | +import { |
| 7 | + setupAtaTE, |
| 8 | + setupMintTE, |
| 9 | + setupMintTEFee, |
| 10 | +} from "./utils/tokenExtensions"; |
| 11 | +import { |
| 12 | + setupPosition, |
| 13 | + setupTEPosition, |
| 14 | + setupWhirlpool, |
| 15 | +} from "./utils/program"; |
| 16 | +import { |
| 17 | + fetchLockConfig, |
| 18 | + getPositionAddress, |
| 19 | + LockType, |
| 20 | +} from "@orca-so/whirlpools-client"; |
| 21 | +import { lockPositionInstructions } from "../src/lockPosition"; |
| 22 | +import { getLockConfigAddress } from "../../client/src/pda/lockConfig"; |
| 23 | +import { |
| 24 | + fetchMaybeMint, |
| 25 | + findAssociatedTokenPda, |
| 26 | +} from "@solana-program/token-2022"; |
| 27 | + |
| 28 | +const mintTypes = new Map([ |
| 29 | + ["A", setupMint], |
| 30 | + ["B", setupMint], |
| 31 | + ["TEA", setupMintTE], |
| 32 | + ["TEB", setupMintTE], |
| 33 | + ["TEFee", setupMintTEFee], |
| 34 | +]); |
| 35 | + |
| 36 | +const ataTypes = new Map([ |
| 37 | + ["A", setupAta], |
| 38 | + ["B", setupAta], |
| 39 | + ["TEA", setupAtaTE], |
| 40 | + ["TEB", setupAtaTE], |
| 41 | + ["TEFee", setupAtaTE], |
| 42 | +]); |
| 43 | + |
| 44 | +const poolTypes = new Map([ |
| 45 | + ["A-B", setupWhirlpool], |
| 46 | + ["A-TEA", setupWhirlpool], |
| 47 | + ["TEA-TEB", setupWhirlpool], |
| 48 | + ["A-TEFee", setupWhirlpool], |
| 49 | +]); |
| 50 | + |
| 51 | +const positionTypes = new Map([ |
| 52 | + ["equally centered", { tickLower: -100, tickUpper: 100 }], |
| 53 | + ["one sided A", { tickLower: -100, tickUpper: -1 }], |
| 54 | + ["one sided B", { tickLower: 1, tickUpper: 100 }], |
| 55 | +]); |
| 56 | + |
| 57 | +describe("LockPosition instructions", () => { |
| 58 | + const tickSpacing = 64; |
| 59 | + const tokenBalance = 1_000_000n; |
| 60 | + const initialLiquidity = 100_000n; |
| 61 | + const mints: Map<string, Address> = new Map(); |
| 62 | + const atas: Map<string, Address> = new Map(); |
| 63 | + const pools: Map<string, Address> = new Map(); |
| 64 | + const positions: Map<string, Address> = new Map(); |
| 65 | + |
| 66 | + beforeAll(async () => { |
| 67 | + for (const [name, setup] of mintTypes) { |
| 68 | + mints.set(name, await setup()); |
| 69 | + } |
| 70 | + |
| 71 | + for (const [name, setup] of ataTypes) { |
| 72 | + const mint = mints.get(name)!; |
| 73 | + atas.set(name, await setup(mint, { amount: tokenBalance })); |
| 74 | + } |
| 75 | + |
| 76 | + for (const [name, setup] of poolTypes) { |
| 77 | + const [mintAKey, mintBKey] = name.split("-"); |
| 78 | + const mintA = mints.get(mintAKey)!; |
| 79 | + const mintB = mints.get(mintBKey)!; |
| 80 | + pools.set(name, await setup(mintA, mintB, tickSpacing)); |
| 81 | + } |
| 82 | + |
| 83 | + for (const [poolName, poolAddress] of pools) { |
| 84 | + for (const [positionTypeName, tickRange] of positionTypes) { |
| 85 | + const position = await setupPosition(poolAddress, { |
| 86 | + ...tickRange, |
| 87 | + liquidity: initialLiquidity, |
| 88 | + }); |
| 89 | + positions.set(`${poolName} ${positionTypeName}`, position); |
| 90 | + |
| 91 | + const positionTE = await setupTEPosition(poolAddress, { |
| 92 | + ...tickRange, |
| 93 | + liquidity: initialLiquidity, |
| 94 | + }); |
| 95 | + positions.set(`TE ${poolName} ${positionTypeName}`, positionTE); |
| 96 | + } |
| 97 | + } |
| 98 | + }); |
| 99 | + |
| 100 | + const testlockPosition = async (poolName: string, positionName: string) => { |
| 101 | + const positionMintAddress = positions.get(positionName)!; |
| 102 | + const [positionAddress] = await getPositionAddress(positionMintAddress); |
| 103 | + const [lockConfigAddress] = await getLockConfigAddress(positionAddress); |
| 104 | + const positionMint = await fetchMaybeMint(rpc, positionMintAddress); |
| 105 | + |
| 106 | + assert(positionMint.exists, "Position mint not found"); |
| 107 | + |
| 108 | + const [positionTokenAccountAddress] = await findAssociatedTokenPda({ |
| 109 | + owner: signer.address, |
| 110 | + mint: positionMintAddress, |
| 111 | + tokenProgram: positionMint.programAddress, |
| 112 | + }); |
| 113 | + const lockPositionInstruction = await lockPositionInstructions({ |
| 114 | + lockType: LockType.Permanent, |
| 115 | + funder: signer, |
| 116 | + positionAuthority: signer, |
| 117 | + position: positionAddress, |
| 118 | + positionMint: positionMintAddress, |
| 119 | + positionTokenAccount: positionTokenAccountAddress, |
| 120 | + lockConfigPda: lockConfigAddress, |
| 121 | + whirlpool: pools.get(poolName)!, |
| 122 | + }); |
| 123 | + |
| 124 | + await sendTransaction(lockPositionInstruction.instructions); |
| 125 | + |
| 126 | + // Verify lock config |
| 127 | + const lockConfig = await fetchLockConfig(rpc, lockConfigAddress); |
| 128 | + assert( |
| 129 | + lockConfig.data.lockType.toFixed() == LockType.Permanent.toFixed(), |
| 130 | + "Lock config lock type is not permanent", |
| 131 | + ); |
| 132 | + assert( |
| 133 | + lockConfig.data.lockedTimestamp > 0, |
| 134 | + "Lock config locked timestamp is not set", |
| 135 | + ); |
| 136 | + assert( |
| 137 | + lockConfig.data.position == positionAddress, |
| 138 | + "Lock config position is not the same as the position", |
| 139 | + ); |
| 140 | + assert( |
| 141 | + lockConfig.data.positionOwner === signer.address, |
| 142 | + "Lock config position owner is not the same as the signer", |
| 143 | + ); |
| 144 | + }; |
| 145 | + |
| 146 | + for (const poolName of poolTypes.keys()) { |
| 147 | + for (const positionTypeName of positionTypes.keys()) { |
| 148 | + const positionNameTE = `TE ${poolName} ${positionTypeName}`; |
| 149 | + it(`Should be able to lock position for ${positionNameTE}`, async () => { |
| 150 | + await testlockPosition(poolName, positionNameTE); |
| 151 | + }); |
| 152 | + } |
| 153 | + } |
| 154 | +}); |
0 commit comments