From 2a668272327cd989c5a0036cc108eec1d292119d Mon Sep 17 00:00:00 2001 From: siddu Date: Tue, 13 Jan 2026 11:28:56 -0500 Subject: [PATCH 1/3] fix: Add sqrtPrice validation in CreatePool function - Reject sqrtPrice = 0 (must be greater than zero) - Reject sqrtPrice outside valid tick range [MIN_SQRT_RATIO, MAX_SQRT_RATIO] - MIN_SQRT_RATIO = 5.42e-20 (tick -887272) - MAX_SQRT_RATIO = 1.84e+19 (tick +887272) Added unit tests for: - Zero value rejection - Below minimum rejection - Above maximum rejection - Boundary acceptance tests --- src/chaincode/dex/createPool.spec.ts | 161 +++++++- src/chaincode/dex/createPool.ts | 18 +- test/shared/constants.ts | 79 ++++ test/shared/fixtures.ts | 576 +++++++++++++++++++++++++++ test/shared/index.ts | 47 +++ test/unit/createPool.spec.ts | 503 +++++++++++++++++++++++ 6 files changed, 1382 insertions(+), 2 deletions(-) create mode 100644 test/shared/constants.ts create mode 100644 test/shared/fixtures.ts create mode 100644 test/shared/index.ts create mode 100644 test/unit/createPool.spec.ts diff --git a/src/chaincode/dex/createPool.spec.ts b/src/chaincode/dex/createPool.spec.ts index 8f52e15..df89a0d 100644 --- a/src/chaincode/dex/createPool.spec.ts +++ b/src/chaincode/dex/createPool.spec.ts @@ -26,7 +26,8 @@ import { currency, fixture, users, writesMap } from "@gala-chain/test"; import BigNumber from "bignumber.js"; import { plainToInstance } from "class-transformer"; -import { CreatePoolDto, CreatePoolResDto, DexFeeConfig, DexFeePercentageTypes, Pool } from "../../api/"; +import { CreatePoolDto, CreatePoolResDto, DexFeeConfig, DexFeePercentageTypes, Pool, TickData } from "../../api/"; +import { tickToSqrtPrice } from "../../api/utils/dex/tick.helper"; import { DexV3Contract } from "../DexV3Contract"; import dexTestUtils from "../test/dex"; import { generateKeyFromClassKey } from "./dexUtils"; @@ -151,4 +152,162 @@ describe("createPool", () => { expect(response).toEqual(GalaChainResponse.Success(expectedResponse)); expect(getWrites()).toEqual(writesMap(expectedFeeThresholdUses, expectedPool)); }); + + describe("sqrtPrice validation", () => { + it("should reject sqrtPrice = 0", async () => { + const currencyClass: TokenClass = currency.tokenClass(); + const currencyClassKey: TokenClassKey = currency.tokenClassKey(); + + const dexClass: TokenClass = dexTestUtils.tokenClass(); + const dexClassKey: TokenClassKey = dexTestUtils.tokenClassKey(); + + const dexFeeConfig: DexFeeConfig = new DexFeeConfig([asValidUserAlias(users.admin.identityKey)], 0.1); + + const { ctx, contract } = fixture(DexV3Contract) + .registeredUsers(users.testUser1) + .savedState(currencyClass, dexFeeConfig, dexClass); + + const dto = new CreatePoolDto( + dexClassKey, + currencyClassKey, + DexFeePercentageTypes.FEE_1_PERCENT, + new BigNumber(0) // Zero sqrtPrice - should be rejected + ); + dto.uniqueKey = "test-zero"; + dto.sign(users.testUser1.privateKey); + + // When + const response = await contract.CreatePool(ctx, dto); + + // Then - should fail with validation error + expect(response.Status).toBe(0); + expect(response.Message).toContain("initialSqrtPrice must be greater than 0"); + }); + + it("should reject sqrtPrice below MIN_SQRT_RATIO", async () => { + const currencyClass: TokenClass = currency.tokenClass(); + const currencyClassKey: TokenClassKey = currency.tokenClassKey(); + + const dexClass: TokenClass = dexTestUtils.tokenClass(); + const dexClassKey: TokenClassKey = dexTestUtils.tokenClassKey(); + + const dexFeeConfig: DexFeeConfig = new DexFeeConfig([asValidUserAlias(users.admin.identityKey)], 0.1); + + const { ctx, contract } = fixture(DexV3Contract) + .registeredUsers(users.testUser1) + .savedState(currencyClass, dexFeeConfig, dexClass); + + const belowMinSqrtPrice = new BigNumber("1e-50"); // Way below MIN_SQRT_RATIO + + const dto = new CreatePoolDto( + dexClassKey, + currencyClassKey, + DexFeePercentageTypes.FEE_1_PERCENT, + belowMinSqrtPrice + ); + dto.uniqueKey = "test-below-min"; + dto.sign(users.testUser1.privateKey); + + // When + const response = await contract.CreatePool(ctx, dto); + + // Then - should fail with validation error + expect(response.Status).toBe(0); + expect(response.Message).toContain("initialSqrtPrice must be between"); + }); + + it("should reject sqrtPrice above MAX_SQRT_RATIO", async () => { + const currencyClass: TokenClass = currency.tokenClass(); + const currencyClassKey: TokenClassKey = currency.tokenClassKey(); + + const dexClass: TokenClass = dexTestUtils.tokenClass(); + const dexClassKey: TokenClassKey = dexTestUtils.tokenClassKey(); + + const dexFeeConfig: DexFeeConfig = new DexFeeConfig([asValidUserAlias(users.admin.identityKey)], 0.1); + + const { ctx, contract } = fixture(DexV3Contract) + .registeredUsers(users.testUser1) + .savedState(currencyClass, dexFeeConfig, dexClass); + + const aboveMaxSqrtPrice = new BigNumber("1e50"); // Way above MAX_SQRT_RATIO + + const dto = new CreatePoolDto( + dexClassKey, + currencyClassKey, + DexFeePercentageTypes.FEE_1_PERCENT, + aboveMaxSqrtPrice + ); + dto.uniqueKey = "test-above-max"; + dto.sign(users.testUser1.privateKey); + + // When + const response = await contract.CreatePool(ctx, dto); + + // Then - should fail with validation error + expect(response.Status).toBe(0); + expect(response.Message).toContain("initialSqrtPrice must be between"); + }); + + it("should accept sqrtPrice at exactly MIN_SQRT_RATIO", async () => { + const currencyClass: TokenClass = currency.tokenClass(); + const currencyClassKey: TokenClassKey = currency.tokenClassKey(); + + const dexClass: TokenClass = dexTestUtils.tokenClass(); + const dexClassKey: TokenClassKey = dexTestUtils.tokenClassKey(); + + const dexFeeConfig: DexFeeConfig = new DexFeeConfig([asValidUserAlias(users.admin.identityKey)], 0.1); + + const { ctx, contract } = fixture(DexV3Contract) + .registeredUsers(users.testUser1) + .savedState(currencyClass, dexFeeConfig, dexClass); + + const minSqrtPrice = tickToSqrtPrice(TickData.MIN_TICK); + + const dto = new CreatePoolDto( + dexClassKey, + currencyClassKey, + DexFeePercentageTypes.FEE_1_PERCENT, + minSqrtPrice + ); + dto.uniqueKey = "test-min"; + dto.sign(users.testUser1.privateKey); + + // When + const response = await contract.CreatePool(ctx, dto); + + // Then + expect(response.Status).toBe(1); // Success + }); + + it("should accept sqrtPrice at exactly MAX_SQRT_RATIO", async () => { + const currencyClass: TokenClass = currency.tokenClass(); + const currencyClassKey: TokenClassKey = currency.tokenClassKey(); + + const dexClass: TokenClass = dexTestUtils.tokenClass(); + const dexClassKey: TokenClassKey = dexTestUtils.tokenClassKey(); + + const dexFeeConfig: DexFeeConfig = new DexFeeConfig([asValidUserAlias(users.admin.identityKey)], 0.1); + + const { ctx, contract } = fixture(DexV3Contract) + .registeredUsers(users.testUser1) + .savedState(currencyClass, dexFeeConfig, dexClass); + + const maxSqrtPrice = tickToSqrtPrice(TickData.MAX_TICK); + + const dto = new CreatePoolDto( + dexClassKey, + currencyClassKey, + DexFeePercentageTypes.FEE_0_05_PERCENT, // Different fee to avoid duplicate pool + maxSqrtPrice + ); + dto.uniqueKey = "test-max"; + dto.sign(users.testUser1.privateKey); + + // When + const response = await contract.CreatePool(ctx, dto); + + // Then + expect(response.Status).toBe(1); // Success + }); + }); }); diff --git a/src/chaincode/dex/createPool.ts b/src/chaincode/dex/createPool.ts index 19b3b6c..62311c5 100644 --- a/src/chaincode/dex/createPool.ts +++ b/src/chaincode/dex/createPool.ts @@ -15,7 +15,8 @@ import { ConflictError, ValidationFailedError } from "@gala-chain/api"; import { GalaChainContext, fetchTokenClass, getObjectByKey, putChainObject } from "@gala-chain/chaincode"; -import { CreatePoolDto, CreatePoolResDto, DexFeeConfig, Pool } from "../../api/"; +import { CreatePoolDto, CreatePoolResDto, DexFeeConfig, Pool, TickData } from "../../api/"; +import { tickToSqrtPrice } from "../../api/utils/dex/tick.helper"; import { generateKeyFromClassKey } from "./dexUtils"; /** @@ -38,6 +39,21 @@ export async function createPool(ctx: GalaChainContext, dto: CreatePoolDto): Pro ); } + // Validate sqrtPrice is greater than zero + if (dto.initialSqrtPrice.lte(0)) { + throw new ValidationFailedError("initialSqrtPrice must be greater than 0"); + } + + // Validate sqrtPrice is within valid tick range + const MIN_SQRT_RATIO = tickToSqrtPrice(TickData.MIN_TICK); + const MAX_SQRT_RATIO = tickToSqrtPrice(TickData.MAX_TICK); + + if (dto.initialSqrtPrice.lt(MIN_SQRT_RATIO) || dto.initialSqrtPrice.gt(MAX_SQRT_RATIO)) { + throw new ValidationFailedError( + `initialSqrtPrice must be between ${MIN_SQRT_RATIO.toExponential(6)} and ${MAX_SQRT_RATIO.toExponential(6)}` + ); + } + const key = ctx.stub.createCompositeKey(DexFeeConfig.INDEX_KEY, []); let protocolFee = 0.1; // default const protocolFeeConfig = await getObjectByKey(ctx, DexFeeConfig, key).catch(() => null); diff --git a/test/shared/constants.ts b/test/shared/constants.ts new file mode 100644 index 0000000..6df152d --- /dev/null +++ b/test/shared/constants.ts @@ -0,0 +1,79 @@ +/* + * Copyright (c) Gala Games Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DexFeePercentageTypes } from "../../src/api"; + +/** + * Fee tiers available for pool creation + * Maps to DexFeePercentageTypes enum values + */ +export const FEE_TIERS = { + LOW: DexFeePercentageTypes.FEE_0_05_PERCENT, // 500 (0.05%) + MEDIUM: DexFeePercentageTypes.FEE_0_3_PERCENT, // 3000 (0.30%) + HIGH: DexFeePercentageTypes.FEE_1_PERCENT // 10000 (1.00%) +} as const; + +/** + * Tick spacing for each fee tier + * Lower fee = tighter tick spacing = more precision + */ +export const TICK_SPACINGS: Record = { + [FEE_TIERS.LOW]: 10, + [FEE_TIERS.MEDIUM]: 60, + [FEE_TIERS.HIGH]: 200 +}; + +/** + * Minimum tick value (from Uniswap V3) + */ +export const MIN_TICK = -887272; + +/** + * Maximum tick value (from Uniswap V3) + */ +export const MAX_TICK = 887272; + +/** + * Approximate MIN_SQRT_RATIO (sqrt(1.0001^MIN_TICK)) + * Calculated from tickToSqrtPrice(MIN_TICK) with full precision + */ +export const MIN_SQRT_RATIO = "5.4212146310449513864e-20"; + +/** + * Approximate MAX_SQRT_RATIO (sqrt(1.0001^MAX_TICK)) + * Calculated from tickToSqrtPrice(MAX_TICK) with full precision + */ +export const MAX_SQRT_RATIO = "18446050711097703530"; + +/** + * Helper to get minimum usable tick for a given tick spacing + */ +export const getMinTick = (tickSpacing: number): number => + Math.ceil(MIN_TICK / tickSpacing) * tickSpacing; + +/** + * Helper to get maximum usable tick for a given tick spacing + */ +export const getMaxTick = (tickSpacing: number): number => + Math.floor(MAX_TICK / tickSpacing) * tickSpacing; + +/** + * Array of all fee tiers for parametric testing + */ +export const ALL_FEE_TIERS = [ + { name: "LOW", fee: FEE_TIERS.LOW, tickSpacing: TICK_SPACINGS[FEE_TIERS.LOW] }, + { name: "MEDIUM", fee: FEE_TIERS.MEDIUM, tickSpacing: TICK_SPACINGS[FEE_TIERS.MEDIUM] }, + { name: "HIGH", fee: FEE_TIERS.HIGH, tickSpacing: TICK_SPACINGS[FEE_TIERS.HIGH] } +]; diff --git a/test/shared/fixtures.ts b/test/shared/fixtures.ts new file mode 100644 index 0000000..568f365 --- /dev/null +++ b/test/shared/fixtures.ts @@ -0,0 +1,576 @@ +/* + * Copyright (c) Gala Games Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TokenBalance, TokenClass, TokenClassKey, TokenInstance, asValidUserAlias } from "@gala-chain/api"; +import { GalaChainContext } from "@gala-chain/chaincode"; +import { currency, fixture, users } from "@gala-chain/test"; +import BigNumber from "bignumber.js"; +import { plainToInstance } from "class-transformer"; + +import { DexFeeConfig, Pool, TickData, tickToSqrtPrice } from "../../src/api"; +import { DexV3Contract } from "../../src/chaincode/DexV3Contract"; +import dex from "../../src/chaincode/test/dex"; +import { FEE_TIERS, TICK_SPACINGS } from "./constants"; + +/** + * Token fixtures containing all token-related objects + */ +export interface TokenFixtures { + // Token 0 (DEX token) + token0Class: TokenClass; + token0Instance: TokenInstance; + token0ClassKey: TokenClassKey; + token0Balance: TokenBalance; + + // Token 1 (Currency token) + token1Class: TokenClass; + token1Instance: TokenInstance; + token1ClassKey: TokenClassKey; + token1Balance: TokenBalance; +} + +/** + * Creates token fixtures for two tokens (token0 and token1) + * Uses the existing dex and currency test utilities + */ +export function createTokenFixtures(): TokenFixtures { + return { + // Token 0 - DEX token + token0Class: dex.tokenClass(), + token0Instance: dex.tokenInstance(), + token0ClassKey: dex.tokenClassKey(), + token0Balance: dex.tokenBalance(), + + // Token 1 - Currency token + token1Class: currency.tokenClass(), + token1Instance: currency.tokenInstance(), + token1ClassKey: currency.tokenClassKey(), + token1Balance: currency.tokenBalance() + }; +} + +/** + * Options for creating a pool fixture + */ +export interface CreatePoolFixtureOptions { + fee?: number; + initialSqrtPrice?: BigNumber; + protocolFee?: number; + includeExistingPool?: boolean; +} + +/** + * Result of creating a pool fixture + */ +export interface PoolFixtureResult { + ctx: GalaChainContext; + contract: ReturnType>["contract"]; + getWrites: () => Record; + tokens: TokenFixtures; + dexFeeConfig: DexFeeConfig; + existingPool?: Pool; +} + +/** + * Creates a complete fixture for CreatePool tests + * + * @param options - Configuration options for the fixture + * @returns Fixture with context, contract, tokens, and optional existing pool + */ +export function createPoolTestFixture(options: CreatePoolFixtureOptions = {}): PoolFixtureResult { + const { + fee = FEE_TIERS.MEDIUM, + initialSqrtPrice = new BigNumber("1"), + protocolFee = 0.1, + includeExistingPool = false + } = options; + + // Create token fixtures + const tokens = createTokenFixtures(); + + // Create fee configuration + const dexFeeConfig = new DexFeeConfig([asValidUserAlias(users.admin.identityKey)], protocolFee); + + // Optionally create an existing pool (for duplicate pool tests) + let existingPool: Pool | undefined; + if (includeExistingPool) { + existingPool = new Pool( + tokens.token0ClassKey.toStringKey(), + tokens.token1ClassKey.toStringKey(), + tokens.token0ClassKey, + tokens.token1ClassKey, + fee, + initialSqrtPrice, + protocolFee + ); + } + + // Create the fixture - pass all items directly to avoid type issues + const fixtureBuilder = fixture(DexV3Contract).registeredUsers( + users.testUser1 + ); + + const { ctx, contract, getWrites } = existingPool + ? fixtureBuilder.savedState( + tokens.token0Class, + tokens.token0Instance, + tokens.token1Class, + tokens.token1Instance, + dexFeeConfig, + existingPool + ) + : fixtureBuilder.savedState( + tokens.token0Class, + tokens.token0Instance, + tokens.token1Class, + tokens.token1Instance, + dexFeeConfig + ); + + return { + ctx, + contract, + getWrites, + tokens, + dexFeeConfig, + existingPool + }; +} + +/** + * Creates a second set of token fixtures with different collection names + * Useful for testing scenarios with different token pairs + */ +export function createAlternateTokenFixtures(): { + tokenClass: TokenClass; + tokenClassKey: TokenClassKey; + tokenInstance: TokenInstance; +} { + const tokenClassKey = plainToInstance(TokenClassKey, { + collection: "ALT", + category: "Unit", + type: "alternate", + additionalKey: "none" + }); + + const tokenClass = plainToInstance(TokenClass, { + ...tokenClassKey, + name: "Alternate Token", + symbol: "ALT", + decimals: 8, + maxSupply: new BigNumber("1000000000"), + maxCapacity: new BigNumber("1000000000"), + totalSupply: new BigNumber("0"), + totalBurned: new BigNumber("0"), + image: "https://example.com/alt.png", + description: "Alternate test token", + isNonFungible: false, + authorities: [users.admin.identityKey] + }); + + const tokenInstance = plainToInstance(TokenInstance, { + ...tokenClassKey, + instance: new BigNumber("0"), + isNonFungible: false + }); + + return { tokenClass, tokenClassKey, tokenInstance }; +} + +/** + * Options for creating an AddLiquidity test fixture + */ +export interface AddLiquidityFixtureOptions { + fee?: number; + initialSqrtPrice?: BigNumber; + initialTick?: number; + protocolFee?: number; + userBalance0?: BigNumber; + userBalance1?: BigNumber; +} + +/** + * Result of creating an AddLiquidity fixture + */ +export interface AddLiquidityFixtureResult { + ctx: GalaChainContext; + contract: ReturnType>["contract"]; + getWrites: () => Record; + tokens: TokenFixtures; + pool: Pool; + dexFeeConfig: DexFeeConfig; + tickSpacing: number; +} + +/** + * Creates a complete fixture for AddLiquidity tests. + * This includes: + * - Token classes and instances for both tokens + * - An initialized pool with the specified parameters + * - User balances for liquidity provision + * + * @param options - Configuration options for the fixture + * @returns Fixture with context, contract, tokens, and initialized pool + */ +export function createAddLiquidityTestFixture(options: AddLiquidityFixtureOptions = {}): AddLiquidityFixtureResult { + const { + fee = FEE_TIERS.MEDIUM, + initialSqrtPrice = new BigNumber("1"), + protocolFee = 0.1, + userBalance0 = new BigNumber("100000"), + userBalance1 = new BigNumber("100000") + } = options; + + // Get tick spacing for this fee tier + const tickSpacing = TICK_SPACINGS[fee] ?? 60; + + // Create token fixtures + const tokens = createTokenFixtures(); + + // Create fee configuration + const dexFeeConfig = new DexFeeConfig([asValidUserAlias(users.admin.identityKey)], protocolFee); + + // Create the pool (AddLiquidity requires an existing pool) + const pool = new Pool( + tokens.token0ClassKey.toStringKey(), + tokens.token1ClassKey.toStringKey(), + tokens.token0ClassKey, + tokens.token1ClassKey, + fee, + initialSqrtPrice, + protocolFee + ); + + // Create user balances for testUser1 with sufficient tokens + const user1Token0Balance = plainToInstance(TokenBalance, { + ...tokens.token0ClassKey, + owner: users.testUser1.identityKey, + inUseHolds: [], + lockedHolds: [], + instanceIds: [], + quantity: userBalance0 + }); + + const user1Token1Balance = plainToInstance(TokenBalance, { + ...tokens.token1ClassKey, + owner: users.testUser1.identityKey, + inUseHolds: [], + lockedHolds: [], + instanceIds: [], + quantity: userBalance1 + }); + + // Create the fixture with pool and user balances + const { ctx, contract, getWrites } = fixture(DexV3Contract) + .registeredUsers(users.testUser1) + .savedState( + tokens.token0Class, + tokens.token0Instance, + tokens.token1Class, + tokens.token1Instance, + dexFeeConfig, + pool, + user1Token0Balance, + user1Token1Balance + ); + + return { + ctx, + contract, + getWrites, + tokens, + pool, + dexFeeConfig, + tickSpacing + }; +} + +/** + * Helper to calculate aligned ticks for a given fee tier. + * In Uniswap V3, ticks must be multiples of the tick spacing. + * + * @param targetTick - The desired tick value + * @param tickSpacing - The tick spacing for the fee tier + * @returns The nearest aligned tick (rounded down) + */ +export function alignTick(targetTick: number, tickSpacing: number): number { + return Math.floor(targetTick / tickSpacing) * tickSpacing; +} + +/** + * Common tick ranges for testing different position scenarios + */ +export const TEST_TICK_RANGES = { + // Full range position (maximum range) + // Note: MIN_TICK must be aligned UP (ceiling) to stay within bounds, + // MAX_TICK must be aligned DOWN (floor) to stay within bounds + FULL_RANGE: (tickSpacing: number) => ({ + tickLower: Math.ceil(TickData.MIN_TICK / tickSpacing) * tickSpacing, + tickUpper: Math.floor(TickData.MAX_TICK / tickSpacing) * tickSpacing + }), + + // Position around current price (tick 0 = price 1.0) + AROUND_CURRENT: (tickSpacing: number) => ({ + tickLower: alignTick(-1000, tickSpacing), + tickUpper: alignTick(1000, tickSpacing) + }), + + // Position below current price (only provides token1) + BELOW_CURRENT: (tickSpacing: number) => ({ + tickLower: alignTick(-2000, tickSpacing), + tickUpper: alignTick(-500, tickSpacing) + }), + + // Position above current price (only provides token0) + ABOVE_CURRENT: (tickSpacing: number) => ({ + tickLower: alignTick(500, tickSpacing), + tickUpper: alignTick(2000, tickSpacing) + }), + + // Narrow range position + NARROW: (tickSpacing: number) => ({ + tickLower: alignTick(-100, tickSpacing), + tickUpper: alignTick(100, tickSpacing) + }), + + // Wide range position + WIDE: (tickSpacing: number) => ({ + tickLower: alignTick(-10000, tickSpacing), + tickUpper: alignTick(10000, tickSpacing) + }) +}; + +/** + * Helper to get the current tick from sqrtPrice + */ +export function getCurrentTickFromSqrtPrice(sqrtPrice: BigNumber): number { + // tick = log(sqrtPrice^2) / log(1.0001) + // For sqrtPrice = 1, tick = 0 + const price = sqrtPrice.pow(2); + const tick = Math.floor(Math.log(price.toNumber()) / Math.log(1.0001)); + return tick; +} + +/** + * Options for creating a Swap test fixture + */ +export interface SwapTestFixtureOptions { + fee?: number; + initialSqrtPrice?: BigNumber; + protocolFee?: number; + poolLiquidity?: BigNumber; + userBalance0?: BigNumber; + userBalance1?: BigNumber; + poolBalance0?: BigNumber; + poolBalance1?: BigNumber; + bitmap?: Record; + feeGrowthGlobal0?: BigNumber; + feeGrowthGlobal1?: BigNumber; + tickData?: TickData[]; + /** + * Factory function to create tick data with the correct pool hash. + * This solves the chicken-and-egg problem where tick data needs the + * pool hash, but the pool hash isn't known until after pool creation. + * + * @param poolHash - The actual pool hash from the created pool + * @returns Array of TickData objects to include in saved state + */ + tickDataFactory?: (poolHash: string) => TickData[]; +} + +/** + * Result of creating a Swap fixture + */ +export interface SwapTestFixtureResult { + ctx: GalaChainContext; + contract: ReturnType>["contract"]; + getWrites: () => Record; + tokens: TokenFixtures; + pool: Pool; + dexFeeConfig: DexFeeConfig; + tickSpacing: number; + poolAlias: string; +} + +/** + * Creates a complete fixture for Swap tests. + * This includes: + * - Token classes and instances for both tokens + * - An initialized pool with liquidity + * - User and pool balances for swap execution + * + * @param options - Configuration options for the fixture + * @returns Fixture with context, contract, tokens, and initialized pool + */ +export function createSwapTestFixture(options: SwapTestFixtureOptions = {}): SwapTestFixtureResult { + const { + fee = FEE_TIERS.MEDIUM, + initialSqrtPrice = new BigNumber("1"), + protocolFee = 0.1, + poolLiquidity = new BigNumber("1000000"), + userBalance0 = new BigNumber("100000"), + userBalance1 = new BigNumber("100000"), + poolBalance0 = new BigNumber("100000"), + poolBalance1 = new BigNumber("100000"), + bitmap = {}, + feeGrowthGlobal0 = new BigNumber("0"), + feeGrowthGlobal1 = new BigNumber("0"), + tickData = [], + tickDataFactory + } = options; + + // Get tick spacing for this fee tier + const tickSpacing = TICK_SPACINGS[fee] ?? 60; + + // Create token fixtures + const tokens = createTokenFixtures(); + + // Create fee configuration + const dexFeeConfig = new DexFeeConfig([asValidUserAlias(users.admin.identityKey)], protocolFee); + + // Create the pool with liquidity + const pool = new Pool( + tokens.token0ClassKey.toStringKey(), + tokens.token1ClassKey.toStringKey(), + tokens.token0ClassKey, + tokens.token1ClassKey, + fee, + initialSqrtPrice, + protocolFee + ); + + // Set pool state + pool.liquidity = poolLiquidity; + pool.grossPoolLiquidity = poolLiquidity; + pool.bitmap = bitmap; + pool.feeGrowthGlobal0 = feeGrowthGlobal0; + pool.feeGrowthGlobal1 = feeGrowthGlobal1; + + const poolAlias = pool.getPoolAlias(); + + // Create pool balances + const poolToken0Balance = plainToInstance(TokenBalance, { + ...tokens.token0ClassKey, + owner: poolAlias, + inUseHolds: [], + lockedHolds: [], + instanceIds: [], + quantity: poolBalance0 + }); + + const poolToken1Balance = plainToInstance(TokenBalance, { + ...tokens.token1ClassKey, + owner: poolAlias, + inUseHolds: [], + lockedHolds: [], + instanceIds: [], + quantity: poolBalance1 + }); + + // Create user balances for testUser1 + const user1Token0Balance = plainToInstance(TokenBalance, { + ...tokens.token0ClassKey, + owner: users.testUser1.identityKey, + inUseHolds: [], + lockedHolds: [], + instanceIds: [], + quantity: userBalance0 + }); + + const user1Token1Balance = plainToInstance(TokenBalance, { + ...tokens.token1ClassKey, + owner: users.testUser1.identityKey, + inUseHolds: [], + lockedHolds: [], + instanceIds: [], + quantity: userBalance1 + }); + + // Generate tick data using factory if provided (allows using correct pool hash) + const generatedTickData = tickDataFactory ? tickDataFactory(pool.genPoolHash()) : []; + + // Build saved state with all objects + const savedObjects: any[] = [ + tokens.token0Class, + tokens.token0Instance, + tokens.token1Class, + tokens.token1Instance, + dexFeeConfig, + pool, + poolToken0Balance, + poolToken1Balance, + user1Token0Balance, + user1Token1Balance, + ...tickData, + ...generatedTickData + ]; + + // Create the fixture + const { ctx, contract, getWrites } = fixture(DexV3Contract) + .registeredUsers(users.testUser1) + .savedState(...savedObjects); + + return { + ctx, + contract, + getWrites, + tokens, + pool, + dexFeeConfig, + tickSpacing, + poolAlias + }; +} + +/** + * Creates a pool fixture with initialized liquidity position. + * Sets up a pool with liquidity in a specific tick range so swaps can execute. + * + * @param options - Configuration options for the fixture + * @returns Fixture with pool containing active liquidity + */ +export function createSwapTestFixtureWithLiquidity(options: SwapTestFixtureOptions = {}): SwapTestFixtureResult { + const fee = options.fee ?? FEE_TIERS.MEDIUM; + const tickSpacing = TICK_SPACINGS[fee] ?? 60; + + // Create bitmap with initialized ticks around the current price + const tickLower = alignTick(-1000, tickSpacing); + const tickUpper = alignTick(1000, tickSpacing); + + // Calculate word and bit positions for bitmap + const lowerWord = Math.floor(tickLower / tickSpacing / 256); + const lowerBit = ((tickLower / tickSpacing) % 256 + 256) % 256; + const upperWord = Math.floor(tickUpper / tickSpacing / 256); + const upperBit = ((tickUpper / tickSpacing) % 256 + 256) % 256; + + const bitmap: Record = {}; + bitmap[lowerWord.toString()] = (BigInt(1) << BigInt(lowerBit)).toString(); + if (upperWord !== lowerWord) { + bitmap[upperWord.toString()] = (BigInt(1) << BigInt(upperBit)).toString(); + } else { + bitmap[lowerWord.toString()] = ( + BigInt(bitmap[lowerWord.toString()]) | (BigInt(1) << BigInt(upperBit)) + ).toString(); + } + + const poolLiquidity = options.poolLiquidity ?? new BigNumber("1000000"); + + return createSwapTestFixture({ + ...options, + bitmap, + poolLiquidity, + tickData: [] // Ticks will be created dynamically during swap + }); +} diff --git a/test/shared/index.ts b/test/shared/index.ts new file mode 100644 index 0000000..84e800b --- /dev/null +++ b/test/shared/index.ts @@ -0,0 +1,47 @@ +/* + * Copyright (c) Gala Games Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Constants +export { + FEE_TIERS, + TICK_SPACINGS, + MIN_TICK, + MAX_TICK, + MIN_SQRT_RATIO, + MAX_SQRT_RATIO, + getMinTick, + getMaxTick, + ALL_FEE_TIERS +} from "./constants"; + +// Fixtures +export { + createTokenFixtures, + createPoolTestFixture, + createAlternateTokenFixtures, + createAddLiquidityTestFixture, + createSwapTestFixture, + createSwapTestFixtureWithLiquidity, + alignTick, + getCurrentTickFromSqrtPrice, + TEST_TICK_RANGES, + type TokenFixtures, + type CreatePoolFixtureOptions, + type PoolFixtureResult, + type AddLiquidityFixtureOptions, + type AddLiquidityFixtureResult, + type SwapTestFixtureOptions, + type SwapTestFixtureResult +} from "./fixtures"; diff --git a/test/unit/createPool.spec.ts b/test/unit/createPool.spec.ts new file mode 100644 index 0000000..66d095f --- /dev/null +++ b/test/unit/createPool.spec.ts @@ -0,0 +1,503 @@ +/* + * Copyright (c) Gala Games Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { GalaChainResponse } from "@gala-chain/api"; +import { users } from "@gala-chain/test"; +import BigNumber from "bignumber.js"; + +import { CreatePoolDto, CreatePoolResDto, Pool } from "../../src/api"; +import { generateKeyFromClassKey } from "../../src/chaincode/dex/dexUtils"; +import { + ALL_FEE_TIERS, + FEE_TIERS, + MIN_SQRT_RATIO, + MAX_SQRT_RATIO, + createAlternateTokenFixtures, + createPoolTestFixture +} from "../shared"; + +describe("CreatePool - Comprehensive Tests", () => { + // ============================================================================ + // HAPPY PATH TESTS + // ============================================================================ + describe("Happy Path", () => { + it("should create a new liquidity pool with default parameters", async () => { + // Given + const { ctx, contract, tokens, dexFeeConfig } = createPoolTestFixture(); + + const dto = new CreatePoolDto( + tokens.token0ClassKey, + tokens.token1ClassKey, + FEE_TIERS.MEDIUM, + new BigNumber("1") // 1:1 price ratio + ); + dto.uniqueKey = "test-create-pool"; + dto.sign(users.testUser1.privateKey); + + // Expected pool + const [token0Key, token1Key] = [ + generateKeyFromClassKey(tokens.token0ClassKey), + generateKeyFromClassKey(tokens.token1ClassKey) + ]; + const expectedPool = new Pool( + token0Key, + token1Key, + tokens.token0ClassKey, + tokens.token1ClassKey, + FEE_TIERS.MEDIUM, + new BigNumber("1"), + dexFeeConfig.protocolFee + ); + + const expectedResponse = new CreatePoolResDto( + tokens.token0ClassKey, + tokens.token1ClassKey, + FEE_TIERS.MEDIUM, + expectedPool.genPoolHash(), + expectedPool.getPoolAlias() + ); + + // When + const response = await contract.CreatePool(ctx, dto); + + // Then + expect(response).toEqual(GalaChainResponse.Success(expectedResponse)); + }); + + it("should create pool and verify initial state is correct", async () => { + // Given + const { ctx, contract, getWrites, tokens } = createPoolTestFixture(); + const initialSqrtPrice = new BigNumber("1.5"); // Price ratio ~2.25:1 + + const dto = new CreatePoolDto( + tokens.token0ClassKey, + tokens.token1ClassKey, + FEE_TIERS.MEDIUM, + initialSqrtPrice + ); + dto.uniqueKey = "test-verify-state"; + dto.sign(users.testUser1.privateKey); + + // When + const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + + // Then + expect(response.Status).toBe(1); + + // Verify pool state from writes + const writes = getWrites(); + const poolKeys = Object.keys(writes).filter((key) => key.includes("GCDXCHLPL")); + expect(poolKeys.length).toBe(1); + + const poolData = JSON.parse(writes[poolKeys[0]]); + + // Verify initial pool state + expect(poolData.liquidity).toBe("0"); // No liquidity initially + expect(poolData.sqrtPrice).toBe(initialSqrtPrice.toString()); + expect(poolData.feeGrowthGlobal0).toBe("0"); + expect(poolData.feeGrowthGlobal1).toBe("0"); + // Note: protocolFee0/protocolFee1 are only set when fees are collected + }); + }); + + // ============================================================================ + // FEE TIER VARIATIONS - Parametric Tests + // ============================================================================ + describe("Fee Tier Variations", () => { + ALL_FEE_TIERS.forEach(({ name, fee, tickSpacing }) => { + it(`should create pool with ${name} fee tier (${fee / 10000}%, tickSpacing=${tickSpacing})`, async () => { + // Given + const { ctx, contract, tokens } = createPoolTestFixture({ fee }); + + const dto = new CreatePoolDto(tokens.token0ClassKey, tokens.token1ClassKey, fee, new BigNumber("1")); + dto.uniqueKey = `test-fee-tier-${name}`; + dto.sign(users.testUser1.privateKey); + + // When + const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + + // Then + expect(response.Status).toBe(1); + expect(response.Data).toBeDefined(); + + const result = response.Data as CreatePoolResDto; + expect(result.poolFee).toBe(fee); + }); + }); + }); + + // ============================================================================ + // ERROR CASES + // ============================================================================ + describe("Error Cases", () => { + it("should fail if pool already exists with same tokens and fee", async () => { + // Given - fixture with existing pool + const { ctx, contract, tokens } = createPoolTestFixture({ + fee: FEE_TIERS.MEDIUM, + includeExistingPool: true + }); + + // Try to create same pool again + const dto = new CreatePoolDto( + tokens.token0ClassKey, + tokens.token1ClassKey, + FEE_TIERS.MEDIUM, // Same fee as existing pool + new BigNumber("1") + ); + dto.uniqueKey = "test-duplicate-pool"; + dto.sign(users.testUser1.privateKey); + + // When + const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + + // Then + expect(response.Status).toBe(0); + expect(response.Message).toContain("already exists"); + }); + + it("should fail if token0 equals token1", async () => { + // Given + const { ctx, contract, tokens } = createPoolTestFixture(); + + // Create DTO with same token for both + const dto = new CreatePoolDto( + tokens.token0ClassKey, + tokens.token0ClassKey, // Same as token0! + FEE_TIERS.MEDIUM, + new BigNumber("1") + ); + dto.uniqueKey = "test-same-tokens"; + dto.sign(users.testUser1.privateKey); + + // When + const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + + // Then + expect(response.Status).toBe(0); + // Should fail with validation error about identical tokens + }); + + it("should reject initialSqrtPrice of zero", async () => { + // Given + const { ctx, contract, tokens } = createPoolTestFixture(); + + const dto = new CreatePoolDto( + tokens.token0ClassKey, + tokens.token1ClassKey, + FEE_TIERS.MEDIUM, + new BigNumber("0") // Zero price - should be rejected + ); + dto.uniqueKey = "test-zero-price"; + dto.sign(users.testUser1.privateKey); + + // When + const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + + // Then - Zero price is now correctly rejected + expect(response.Status).toBe(0); + }); + + it("should fail if initialSqrtPrice is negative", async () => { + // Given + const { ctx, contract, tokens } = createPoolTestFixture(); + + const dto = new CreatePoolDto( + tokens.token0ClassKey, + tokens.token1ClassKey, + FEE_TIERS.MEDIUM, + new BigNumber("-1") // Negative price! + ); + dto.uniqueKey = "test-negative-price"; + dto.sign(users.testUser1.privateKey); + + // When + const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + + // Then + expect(response.Status).toBe(0); + // Should fail - negative price is invalid + }); + + it("should fail if token class does not exist", async () => { + // Given + const { ctx, contract, tokens } = createPoolTestFixture(); + + // Create a fake token class key that doesn't exist in saved state + const { tokenClassKey: fakeTokenKey } = createAlternateTokenFixtures(); + + const dto = new CreatePoolDto( + tokens.token0ClassKey, + fakeTokenKey, // This token doesn't exist in state! + FEE_TIERS.MEDIUM, + new BigNumber("1") + ); + dto.uniqueKey = "test-missing-token"; + dto.sign(users.testUser1.privateKey); + + // When + const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + + // Then + expect(response.Status).toBe(0); + // Should fail - token class not found + }); + }); + + // ============================================================================ + // EDGE CASES + // ============================================================================ + describe("Edge Cases", () => { + it("should fail when tokens are passed in wrong order (requires token0 < token1)", async () => { + // Given + const { ctx, contract, tokens } = createPoolTestFixture(); + + // Pass tokens in reverse order (token1 first, then token0) + // The contract expects token0.toStringKey() < token1.toStringKey() + const dto = new CreatePoolDto( + tokens.token1ClassKey, // Swapped - this will cause failure + tokens.token0ClassKey, // Swapped - this will cause failure + FEE_TIERS.MEDIUM, + new BigNumber("1") + ); + dto.uniqueKey = "test-reverse-order"; + dto.sign(users.testUser1.privateKey); + + // When + const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + + // Then - Contract requires tokens in sorted order + expect(response.Status).toBe(0); + }); + + it("should create pool with very small sqrtPrice (near minimum)", async () => { + // Given + const { ctx, contract, tokens } = createPoolTestFixture(); + + const dto = new CreatePoolDto( + tokens.token0ClassKey, + tokens.token1ClassKey, + FEE_TIERS.MEDIUM, + new BigNumber("0.0000001") // Very small price + ); + dto.uniqueKey = "test-small-price"; + dto.sign(users.testUser1.privateKey); + + // When + const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + + // Then + expect(response.Status).toBe(1); + }); + + it("should create pool with very large sqrtPrice (near maximum)", async () => { + // Given + const { ctx, contract, tokens } = createPoolTestFixture(); + + const dto = new CreatePoolDto( + tokens.token0ClassKey, + tokens.token1ClassKey, + FEE_TIERS.MEDIUM, + new BigNumber("10000000") // Very large price + ); + dto.uniqueKey = "test-large-price"; + dto.sign(users.testUser1.privateKey); + + // When + const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + + // Then + expect(response.Status).toBe(1); + }); + + it("should create pool with different protocol fees", async () => { + // Given - test with 0% protocol fee + const { ctx, contract, tokens } = createPoolTestFixture({ protocolFee: 0 }); + + const dto = new CreatePoolDto( + tokens.token0ClassKey, + tokens.token1ClassKey, + FEE_TIERS.MEDIUM, + new BigNumber("1") + ); + dto.uniqueKey = "test-zero-protocol-fee"; + dto.sign(users.testUser1.privateKey); + + // When + const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + + // Then + expect(response.Status).toBe(1); + }); + + it("should accept pool at MIN_SQRT_RATIO (valid minimum price)", async () => { + // Given - Use the minimum sqrt price corresponding to MIN_TICK (-887272) + const { ctx, contract, tokens } = createPoolTestFixture(); + + const dto = new CreatePoolDto( + tokens.token0ClassKey, + tokens.token1ClassKey, + FEE_TIERS.MEDIUM, + new BigNumber(MIN_SQRT_RATIO) // Valid minimum price + ); + dto.uniqueKey = "test-min-sqrt-price"; + dto.sign(users.testUser1.privateKey); + + // When + const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + + // Then - MIN_SQRT_RATIO is valid and should be accepted + expect(response.Status).toBe(1); + }); + + it("should accept pool at MAX_SQRT_RATIO (valid maximum price)", async () => { + // Given - Use the maximum sqrt price corresponding to MAX_TICK (887272) + const { ctx, contract, tokens } = createPoolTestFixture(); + + const dto = new CreatePoolDto( + tokens.token0ClassKey, + tokens.token1ClassKey, + FEE_TIERS.MEDIUM, + new BigNumber(MAX_SQRT_RATIO) // Valid maximum price + ); + dto.uniqueKey = "test-max-sqrt-price"; + dto.sign(users.testUser1.privateKey); + + // When + const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + + // Then - MAX_SQRT_RATIO is valid and should be accepted + expect(response.Status).toBe(1); + }); + + it("should reject pool with price below MIN_SQRT_RATIO", async () => { + // Given - Price smaller than valid minimum + const { ctx, contract, tokens } = createPoolTestFixture(); + const belowMinPrice = new BigNumber(MIN_SQRT_RATIO).dividedBy(1000); // 1000x smaller + + const dto = new CreatePoolDto( + tokens.token0ClassKey, + tokens.token1ClassKey, + FEE_TIERS.MEDIUM, + belowMinPrice + ); + dto.uniqueKey = "test-below-min-sqrt-price"; + dto.sign(users.testUser1.privateKey); + + // When + const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + + // Then - Price below MIN_SQRT_RATIO is now correctly rejected + expect(response.Status).toBe(0); + }); + + it("should reject pool with price above MAX_SQRT_RATIO", async () => { + // Given - Price larger than valid maximum + const { ctx, contract, tokens } = createPoolTestFixture(); + const aboveMaxPrice = new BigNumber(MAX_SQRT_RATIO).multipliedBy(1000); // 1000x larger + + const dto = new CreatePoolDto( + tokens.token0ClassKey, + tokens.token1ClassKey, + FEE_TIERS.MEDIUM, + aboveMaxPrice + ); + dto.uniqueKey = "test-above-max-sqrt-price"; + dto.sign(users.testUser1.privateKey); + + // When + const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + + // Then - Price above MAX_SQRT_RATIO is now correctly rejected + expect(response.Status).toBe(0); + }); + }); + + // ============================================================================ + // STATE VERIFICATION + // ============================================================================ + describe("State Verification", () => { + it("should set creator to the calling user", async () => { + // Given + const { ctx, contract, getWrites, tokens } = createPoolTestFixture(); + + const dto = new CreatePoolDto( + tokens.token0ClassKey, + tokens.token1ClassKey, + FEE_TIERS.MEDIUM, + new BigNumber("1") + ); + dto.uniqueKey = "test-creator"; + dto.sign(users.testUser1.privateKey); + + // When + await contract.CreatePool(ctx, dto); + + // Then + const writes = getWrites(); + const poolKeys = Object.keys(writes).filter((key) => key.includes("GCDXCHLPL")); + const poolData = JSON.parse(writes[poolKeys[0]]); + + expect(poolData.creator).toBe(users.testUser1.identityKey); + }); + + it("should initialize bitmap as empty object", async () => { + // Given + const { ctx, contract, getWrites, tokens } = createPoolTestFixture(); + + const dto = new CreatePoolDto( + tokens.token0ClassKey, + tokens.token1ClassKey, + FEE_TIERS.MEDIUM, + new BigNumber("1") + ); + dto.uniqueKey = "test-bitmap"; + dto.sign(users.testUser1.privateKey); + + // When + await contract.CreatePool(ctx, dto); + + // Then + const writes = getWrites(); + const poolKeys = Object.keys(writes).filter((key) => key.includes("GCDXCHLPL")); + const poolData = JSON.parse(writes[poolKeys[0]]); + + expect(poolData.bitmap).toEqual({}); + }); + + it("should set correct fee tier in pool state", async () => { + // Given + const { ctx, contract, getWrites, tokens } = createPoolTestFixture(); + + const dto = new CreatePoolDto( + tokens.token0ClassKey, + tokens.token1ClassKey, + FEE_TIERS.HIGH, // 1% fee + new BigNumber("1") + ); + dto.uniqueKey = "test-fee-state"; + dto.sign(users.testUser1.privateKey); + + // When + await contract.CreatePool(ctx, dto); + + // Then + const writes = getWrites(); + const poolKeys = Object.keys(writes).filter((key) => key.includes("GCDXCHLPL")); + const poolData = JSON.parse(writes[poolKeys[0]]); + + expect(poolData.fee).toBe(FEE_TIERS.HIGH); + }); + }); +}); From 246db195019adb9fd9bf234df3baec7a9049c590 Mon Sep 17 00:00:00 2001 From: siddu Date: Tue, 13 Jan 2026 13:39:44 -0500 Subject: [PATCH 2/3] fix: Resolve prettier/eslint formatting issues in test files --- test/shared/constants.ts | 7 +--- test/shared/fixtures.ts | 16 +++++--- test/unit/createPool.spec.ts | 78 ++++++++++++++++++++++++++++-------- 3 files changed, 73 insertions(+), 28 deletions(-) diff --git a/test/shared/constants.ts b/test/shared/constants.ts index 6df152d..521e1a5 100644 --- a/test/shared/constants.ts +++ b/test/shared/constants.ts @@ -12,7 +12,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { DexFeePercentageTypes } from "../../src/api"; /** @@ -60,14 +59,12 @@ export const MAX_SQRT_RATIO = "18446050711097703530"; /** * Helper to get minimum usable tick for a given tick spacing */ -export const getMinTick = (tickSpacing: number): number => - Math.ceil(MIN_TICK / tickSpacing) * tickSpacing; +export const getMinTick = (tickSpacing: number): number => Math.ceil(MIN_TICK / tickSpacing) * tickSpacing; /** * Helper to get maximum usable tick for a given tick spacing */ -export const getMaxTick = (tickSpacing: number): number => - Math.floor(MAX_TICK / tickSpacing) * tickSpacing; +export const getMaxTick = (tickSpacing: number): number => Math.floor(MAX_TICK / tickSpacing) * tickSpacing; /** * Array of all fee tiers for parametric testing diff --git a/test/shared/fixtures.ts b/test/shared/fixtures.ts index 568f365..c851bdf 100644 --- a/test/shared/fixtures.ts +++ b/test/shared/fixtures.ts @@ -12,7 +12,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { TokenBalance, TokenClass, TokenClassKey, TokenInstance, asValidUserAlias } from "@gala-chain/api"; import { GalaChainContext } from "@gala-chain/chaincode"; import { currency, fixture, users } from "@gala-chain/test"; @@ -224,7 +223,9 @@ export interface AddLiquidityFixtureResult { * @param options - Configuration options for the fixture * @returns Fixture with context, contract, tokens, and initialized pool */ -export function createAddLiquidityTestFixture(options: AddLiquidityFixtureOptions = {}): AddLiquidityFixtureResult { +export function createAddLiquidityTestFixture( + options: AddLiquidityFixtureOptions = {} +): AddLiquidityFixtureResult { const { fee = FEE_TIERS.MEDIUM, initialSqrtPrice = new BigNumber("1"), @@ -541,7 +542,9 @@ export function createSwapTestFixture(options: SwapTestFixtureOptions = {}): Swa * @param options - Configuration options for the fixture * @returns Fixture with pool containing active liquidity */ -export function createSwapTestFixtureWithLiquidity(options: SwapTestFixtureOptions = {}): SwapTestFixtureResult { +export function createSwapTestFixtureWithLiquidity( + options: SwapTestFixtureOptions = {} +): SwapTestFixtureResult { const fee = options.fee ?? FEE_TIERS.MEDIUM; const tickSpacing = TICK_SPACINGS[fee] ?? 60; @@ -551,9 +554,9 @@ export function createSwapTestFixtureWithLiquidity(options: SwapTestFixtureOptio // Calculate word and bit positions for bitmap const lowerWord = Math.floor(tickLower / tickSpacing / 256); - const lowerBit = ((tickLower / tickSpacing) % 256 + 256) % 256; + const lowerBit = (((tickLower / tickSpacing) % 256) + 256) % 256; const upperWord = Math.floor(tickUpper / tickSpacing / 256); - const upperBit = ((tickUpper / tickSpacing) % 256 + 256) % 256; + const upperBit = (((tickUpper / tickSpacing) % 256) + 256) % 256; const bitmap: Record = {}; bitmap[lowerWord.toString()] = (BigInt(1) << BigInt(lowerBit)).toString(); @@ -561,7 +564,8 @@ export function createSwapTestFixtureWithLiquidity(options: SwapTestFixtureOptio bitmap[upperWord.toString()] = (BigInt(1) << BigInt(upperBit)).toString(); } else { bitmap[lowerWord.toString()] = ( - BigInt(bitmap[lowerWord.toString()]) | (BigInt(1) << BigInt(upperBit)) + BigInt(bitmap[lowerWord.toString()]) | + (BigInt(1) << BigInt(upperBit)) ).toString(); } diff --git a/test/unit/createPool.spec.ts b/test/unit/createPool.spec.ts index 66d095f..374e5f9 100644 --- a/test/unit/createPool.spec.ts +++ b/test/unit/createPool.spec.ts @@ -12,7 +12,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { GalaChainResponse } from "@gala-chain/api"; import { users } from "@gala-chain/test"; import BigNumber from "bignumber.js"; @@ -22,8 +21,8 @@ import { generateKeyFromClassKey } from "../../src/chaincode/dex/dexUtils"; import { ALL_FEE_TIERS, FEE_TIERS, - MIN_SQRT_RATIO, MAX_SQRT_RATIO, + MIN_SQRT_RATIO, createAlternateTokenFixtures, createPoolTestFixture } from "../shared"; @@ -91,7 +90,10 @@ describe("CreatePool - Comprehensive Tests", () => { dto.sign(users.testUser1.privateKey); // When - const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + const response = (await contract.CreatePool( + ctx, + dto + )) as unknown as GalaChainResponse; // Then expect(response.Status).toBe(1); @@ -126,7 +128,10 @@ describe("CreatePool - Comprehensive Tests", () => { dto.sign(users.testUser1.privateKey); // When - const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + const response = (await contract.CreatePool( + ctx, + dto + )) as unknown as GalaChainResponse; // Then expect(response.Status).toBe(1); @@ -160,7 +165,10 @@ describe("CreatePool - Comprehensive Tests", () => { dto.sign(users.testUser1.privateKey); // When - const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + const response = (await contract.CreatePool( + ctx, + dto + )) as unknown as GalaChainResponse; // Then expect(response.Status).toBe(0); @@ -182,7 +190,10 @@ describe("CreatePool - Comprehensive Tests", () => { dto.sign(users.testUser1.privateKey); // When - const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + const response = (await contract.CreatePool( + ctx, + dto + )) as unknown as GalaChainResponse; // Then expect(response.Status).toBe(0); @@ -203,7 +214,10 @@ describe("CreatePool - Comprehensive Tests", () => { dto.sign(users.testUser1.privateKey); // When - const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + const response = (await contract.CreatePool( + ctx, + dto + )) as unknown as GalaChainResponse; // Then - Zero price is now correctly rejected expect(response.Status).toBe(0); @@ -223,7 +237,10 @@ describe("CreatePool - Comprehensive Tests", () => { dto.sign(users.testUser1.privateKey); // When - const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + const response = (await contract.CreatePool( + ctx, + dto + )) as unknown as GalaChainResponse; // Then expect(response.Status).toBe(0); @@ -247,7 +264,10 @@ describe("CreatePool - Comprehensive Tests", () => { dto.sign(users.testUser1.privateKey); // When - const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + const response = (await contract.CreatePool( + ctx, + dto + )) as unknown as GalaChainResponse; // Then expect(response.Status).toBe(0); @@ -275,7 +295,10 @@ describe("CreatePool - Comprehensive Tests", () => { dto.sign(users.testUser1.privateKey); // When - const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + const response = (await contract.CreatePool( + ctx, + dto + )) as unknown as GalaChainResponse; // Then - Contract requires tokens in sorted order expect(response.Status).toBe(0); @@ -295,7 +318,10 @@ describe("CreatePool - Comprehensive Tests", () => { dto.sign(users.testUser1.privateKey); // When - const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + const response = (await contract.CreatePool( + ctx, + dto + )) as unknown as GalaChainResponse; // Then expect(response.Status).toBe(1); @@ -315,7 +341,10 @@ describe("CreatePool - Comprehensive Tests", () => { dto.sign(users.testUser1.privateKey); // When - const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + const response = (await contract.CreatePool( + ctx, + dto + )) as unknown as GalaChainResponse; // Then expect(response.Status).toBe(1); @@ -335,7 +364,10 @@ describe("CreatePool - Comprehensive Tests", () => { dto.sign(users.testUser1.privateKey); // When - const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + const response = (await contract.CreatePool( + ctx, + dto + )) as unknown as GalaChainResponse; // Then expect(response.Status).toBe(1); @@ -355,7 +387,10 @@ describe("CreatePool - Comprehensive Tests", () => { dto.sign(users.testUser1.privateKey); // When - const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + const response = (await contract.CreatePool( + ctx, + dto + )) as unknown as GalaChainResponse; // Then - MIN_SQRT_RATIO is valid and should be accepted expect(response.Status).toBe(1); @@ -375,7 +410,10 @@ describe("CreatePool - Comprehensive Tests", () => { dto.sign(users.testUser1.privateKey); // When - const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + const response = (await contract.CreatePool( + ctx, + dto + )) as unknown as GalaChainResponse; // Then - MAX_SQRT_RATIO is valid and should be accepted expect(response.Status).toBe(1); @@ -396,7 +434,10 @@ describe("CreatePool - Comprehensive Tests", () => { dto.sign(users.testUser1.privateKey); // When - const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + const response = (await contract.CreatePool( + ctx, + dto + )) as unknown as GalaChainResponse; // Then - Price below MIN_SQRT_RATIO is now correctly rejected expect(response.Status).toBe(0); @@ -417,7 +458,10 @@ describe("CreatePool - Comprehensive Tests", () => { dto.sign(users.testUser1.privateKey); // When - const response = (await contract.CreatePool(ctx, dto)) as unknown as GalaChainResponse; + const response = (await contract.CreatePool( + ctx, + dto + )) as unknown as GalaChainResponse; // Then - Price above MAX_SQRT_RATIO is now correctly rejected expect(response.Status).toBe(0); From 4029433938a105e2b53557bd9c4a5a897b836d33 Mon Sep 17 00:00:00 2001 From: siddu Date: Tue, 13 Jan 2026 13:43:07 -0500 Subject: [PATCH 3/3] fix: prettier formatting in createPool.spec.ts --- src/chaincode/dex/createPool.spec.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/chaincode/dex/createPool.spec.ts b/src/chaincode/dex/createPool.spec.ts index df89a0d..aaf205d 100644 --- a/src/chaincode/dex/createPool.spec.ts +++ b/src/chaincode/dex/createPool.spec.ts @@ -26,7 +26,14 @@ import { currency, fixture, users, writesMap } from "@gala-chain/test"; import BigNumber from "bignumber.js"; import { plainToInstance } from "class-transformer"; -import { CreatePoolDto, CreatePoolResDto, DexFeeConfig, DexFeePercentageTypes, Pool, TickData } from "../../api/"; +import { + CreatePoolDto, + CreatePoolResDto, + DexFeeConfig, + DexFeePercentageTypes, + Pool, + TickData +} from "../../api/"; import { tickToSqrtPrice } from "../../api/utils/dex/tick.helper"; import { DexV3Contract } from "../DexV3Contract"; import dexTestUtils from "../test/dex";