diff --git a/.github/workflows/publish_oraiswap_v3.yml b/.github/workflows/publish_oraiswap_v3.yml new file mode 100644 index 00000000..0be79070 --- /dev/null +++ b/.github/workflows/publish_oraiswap_v3.yml @@ -0,0 +1,67 @@ +name: publish_package_oraiswap_v3 + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events but only for the main branch + push: + branches: [feat/zapper-2] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + build: + runs-on: ubuntu-20.04 + strategy: + matrix: + node-version: ["18"] + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.8.0 + with: + access_token: ${{ github.token }} + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + - uses: actions/cache@v4 + id: yarn-cache + with: + path: | + ${{ steps.yarn-cache-dir-path.outputs.dir }} + ./node_modules/ + key: ${{ runner.os }}-yarn-${{ hashFiles('./yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: Install Dependencies + run: yarn + - name: Build + run: yarn build + - name: Authenticate with private NPM package + run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc + - name: Publish Oraiswap v3 + id: publish-oraiswap-v3 + continue-on-error: true + run: yarn deploy:beta packages/oraiswap-v3 + env: + CI: false + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Publish Contract SDK + id: publish-oraidex-contracts-sdk + continue-on-error: true + run: yarn deploy:beta packages/contracts-sdk + env: + CI: false + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Publish Contract Build + id: publish-oraidex-contracts-build + continue-on-error: true + run: yarn deploy:beta packages/contracts-build + env: + CI: false + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/packages/contracts-sdk/package.json b/packages/contracts-sdk/package.json index 30cc4a69..bc02eaf9 100644 --- a/packages/contracts-sdk/package.json +++ b/packages/contracts-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@oraichain/oraidex-contracts-sdk", - "version": "1.0.52", + "version": "1.0.53-beta.2", "main": "build/index.js", "files": [ "build/", diff --git a/packages/contracts-sdk/src/IncentivesFundManager.client.ts b/packages/contracts-sdk/src/IncentivesFundManager.client.ts new file mode 100644 index 00000000..cb712efc --- /dev/null +++ b/packages/contracts-sdk/src/IncentivesFundManager.client.ts @@ -0,0 +1,90 @@ +/** +* This file was automatically generated by @oraichain/ts-codegen@0.35.9. +* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, +* and run the @oraichain/ts-codegen generate command to regenerate this file. +*/ + +import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult } from "@cosmjs/cosmwasm-stargate"; +import { Coin, StdFee } from "@cosmjs/amino"; +import {Addr, InstantiateMsg, ExecuteMsg, Uint128, AssetInfo, Asset, QueryMsg, MigrateMsg, ConfigResponse} from "./IncentivesFundManager.types"; +export interface IncentivesFundManagerReadOnlyInterface { + contractAddress: string; + config: () => Promise; +} +export class IncentivesFundManagerQueryClient implements IncentivesFundManagerReadOnlyInterface { + client: CosmWasmClient; + contractAddress: string; + + constructor(client: CosmWasmClient, contractAddress: string) { + this.client = client; + this.contractAddress = contractAddress; + this.config = this.config.bind(this); + } + + config = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + config: {} + }); + }; +} +export interface IncentivesFundManagerInterface extends IncentivesFundManagerReadOnlyInterface { + contractAddress: string; + sender: string; + updateConfig: ({ + oraiswapV3, + owner + }: { + oraiswapV3?: Addr; + owner?: Addr; + }, _fee?: number | StdFee | "auto", _memo?: string, _funds?: Coin[]) => Promise; + sendFund: ({ + asset, + receiver + }: { + asset: Asset; + receiver: Addr; + }, _fee?: number | StdFee | "auto", _memo?: string, _funds?: Coin[]) => Promise; +} +export class IncentivesFundManagerClient extends IncentivesFundManagerQueryClient implements IncentivesFundManagerInterface { + client: SigningCosmWasmClient; + sender: string; + contractAddress: string; + + constructor(client: SigningCosmWasmClient, sender: string, contractAddress: string) { + super(client, contractAddress); + this.client = client; + this.sender = sender; + this.contractAddress = contractAddress; + this.updateConfig = this.updateConfig.bind(this); + this.sendFund = this.sendFund.bind(this); + } + + updateConfig = async ({ + oraiswapV3, + owner + }: { + oraiswapV3?: Addr; + owner?: Addr; + }, _fee: number | StdFee | "auto" = "auto", _memo?: string, _funds?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + update_config: { + oraiswap_v3: oraiswapV3, + owner + } + }, _fee, _memo, _funds); + }; + sendFund = async ({ + asset, + receiver + }: { + asset: Asset; + receiver: Addr; + }, _fee: number | StdFee | "auto" = "auto", _memo?: string, _funds?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + send_fund: { + asset, + receiver + } + }, _fee, _memo, _funds); + }; +} \ No newline at end of file diff --git a/packages/contracts-sdk/src/IncentivesFundManager.types.ts b/packages/contracts-sdk/src/IncentivesFundManager.types.ts new file mode 100644 index 00000000..700fca3a --- /dev/null +++ b/packages/contracts-sdk/src/IncentivesFundManager.types.ts @@ -0,0 +1,38 @@ +export type Addr = string; +export interface InstantiateMsg { + oraiswap_v3: Addr; + owner?: Addr | null; +} +export type ExecuteMsg = { + update_config: { + oraiswap_v3?: Addr | null; + owner?: Addr | null; + }; +} | { + send_fund: { + asset: Asset; + receiver: Addr; + }; +}; +export type Uint128 = string; +export type AssetInfo = { + token: { + contract_addr: Addr; + }; +} | { + native_token: { + denom: string; + }; +}; +export interface Asset { + amount: Uint128; + info: AssetInfo; +} +export type QueryMsg = { + config: {}; +}; +export interface MigrateMsg {} +export interface ConfigResponse { + oraiswap_v3: Addr; + owner: Addr; +} \ No newline at end of file diff --git a/packages/contracts-sdk/src/OraiswapV3.client.ts b/packages/contracts-sdk/src/OraiswapV3.client.ts index 5992663d..a97d2971 100644 --- a/packages/contracts-sdk/src/OraiswapV3.client.ts +++ b/packages/contracts-sdk/src/OraiswapV3.client.ts @@ -6,11 +6,12 @@ import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult } from "@cosmjs/cosmwasm-stargate"; import { Coin, StdFee } from "@cosmjs/amino"; -import {Percentage, InstantiateMsg, ExecuteMsg, Addr, Liquidity, SqrtPrice, TokenAmount, Binary, Expiration, Timestamp, Uint64, AssetInfo, PoolKey, FeeTier, SwapHop, NftExtensionMsg, QueryMsg, MigrateMsg, FeeGrowth, AllNftInfoResponse, OwnerOfResponse, Approval, NftInfoResponse, Position, PositionIncentives, ArrayOfPosition, TokensResponse, ApprovedForAllResponse, Boolean, ArrayOfFeeTier, ArrayOfLiquidityTick, LiquidityTick, Uint32, NumTokensResponse, Pool, IncentiveRecord, ArrayOfPoolWithPoolKey, PoolWithPoolKey, Uint128, ArrayOfAsset, Asset, ArrayOfPositionTick, PositionTick, QuoteResult, Tick, TickIncentive, ArrayOfTupleOfUint16AndUint64} from "./OraiswapV3.types"; +import {Addr, Percentage, InstantiateMsg, ExecuteMsg, Liquidity, SqrtPrice, TokenAmount, Binary, Expiration, Timestamp, Uint64, AssetInfo, PoolKey, FeeTier, SwapHop, NftExtensionMsg, QueryMsg, MigrateMsg, FeeGrowth, AllNftInfoResponse, OwnerOfResponse, Approval, NftInfoResponse, Position, PositionIncentives, ArrayOfPosition, TokensResponse, ApprovedForAllResponse, Boolean, ArrayOfFeeTier, ArrayOfLiquidityTick, LiquidityTick, Uint32, NumTokensResponse, Pool, IncentiveRecord, ArrayOfPoolWithPoolKey, PoolWithPoolKey, Uint128, ArrayOfAsset, Asset, ArrayOfPositionTick, PositionTick, QuoteResult, Tick, TickIncentive, ArrayOfTupleOfUint16AndUint64} from "./OraiswapV3.types"; export interface OraiswapV3ReadOnlyInterface { contractAddress: string; admin: () => Promise; protocolFee: () => Promise; + incentivesFundManager: () => Promise; position: ({ index, ownerId @@ -205,6 +206,7 @@ export class OraiswapV3QueryClient implements OraiswapV3ReadOnlyInterface { this.contractAddress = contractAddress; this.admin = this.admin.bind(this); this.protocolFee = this.protocolFee.bind(this); + this.incentivesFundManager = this.incentivesFundManager.bind(this); this.position = this.position.bind(this); this.positions = this.positions.bind(this); this.allPosition = this.allPosition.bind(this); @@ -243,6 +245,11 @@ export class OraiswapV3QueryClient implements OraiswapV3ReadOnlyInterface { protocol_fee: {} }); }; + incentivesFundManager = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + incentives_fund_manager: {} + }); + }; position = async ({ index, ownerId diff --git a/packages/contracts-sdk/src/OraiswapV3.types.ts b/packages/contracts-sdk/src/OraiswapV3.types.ts index 613c0aec..bad12381 100644 --- a/packages/contracts-sdk/src/OraiswapV3.types.ts +++ b/packages/contracts-sdk/src/OraiswapV3.types.ts @@ -1,5 +1,7 @@ +export type Addr = string; export type Percentage = number; export interface InstantiateMsg { + incentives_fund_manager: Addr; protocol_fee: Percentage; } export type ExecuteMsg = { @@ -132,7 +134,6 @@ export type ExecuteMsg = { index: number; }; }; -export type Addr = string; export type Liquidity = string; export type SqrtPrice = string; export type TokenAmount = string; @@ -180,6 +181,8 @@ export type QueryMsg = { admin: {}; } | { protocol_fee: {}; +} | { + incentives_fund_manager: {}; } | { position: { index: number; diff --git a/packages/contracts-sdk/src/index.ts b/packages/contracts-sdk/src/index.ts index 8d981df6..a83ce87b 100644 --- a/packages/contracts-sdk/src/index.ts +++ b/packages/contracts-sdk/src/index.ts @@ -32,4 +32,6 @@ export * as OraiswapMixedRouterTypes from "./OraiswapMixedRouter.types"; export * from "./OraiswapMixedRouter.client"; export * as ZapperTypes from "./Zapper.types"; export * from "./Zapper.client"; +export * as IncentivesFundManagerTypes from "./IncentivesFundManager.types"; +export * from "./IncentivesFundManager.client"; export * from "./types"; diff --git a/packages/oraiswap-v3/package.json b/packages/oraiswap-v3/package.json index 72a320c1..60acf7d9 100644 --- a/packages/oraiswap-v3/package.json +++ b/packages/oraiswap-v3/package.json @@ -1,6 +1,6 @@ { "name": "@oraichain/oraiswap-v3", - "version": "0.1.20", + "version": "0.1.20-beta.2", "main": "build/index.js", "files": [ "build/" @@ -12,7 +12,7 @@ "license": "MIT", "dependencies": { "@cosmjs/cosmwasm-stargate": "^0.31.0", - "@oraichain/oraidex-contracts-sdk": "^1.0.52", + "@oraichain/oraidex-contracts-sdk": "1.0.53-beta.2", "@oraichain/oraidex-common": "^1.1.21" } } diff --git a/packages/oraiswap-v3/src/error.ts b/packages/oraiswap-v3/src/error.ts index 43ece874..89e21651 100644 --- a/packages/oraiswap-v3/src/error.ts +++ b/packages/oraiswap-v3/src/error.ts @@ -2,4 +2,16 @@ export class RouteNotFoundError extends Error { constructor(route: string) { super(`Route not found: ${route}`); } +} + +export class RouteNoLiquidity extends Error { + constructor() { + super(`Route has no liquidity`); + } +} + +export class SpamTooManyRequestsError extends Error { + constructor() { + super(`Too many requests`); + } } \ No newline at end of file diff --git a/packages/oraiswap-v3/src/helpers.ts b/packages/oraiswap-v3/src/helpers.ts index 61d308bb..6a38980e 100644 --- a/packages/oraiswap-v3/src/helpers.ts +++ b/packages/oraiswap-v3/src/helpers.ts @@ -496,30 +496,52 @@ export const extractOraidexV3Actions = (routes: RouteResponse[]): ActionRoute[] return routesArray; }; +export type buildZapInMessageOptions = { + isSingleSide: boolean; + isTokenX: boolean; +}; + export const populateMessageZapIn = ( message: ZapInLiquidityResponse, - tokenIn: TokenItemType, - amountIn: string, - amountInToX: bigint, - amountInToY: bigint, actualAmountXReceived: SmartRouteResponse, actualAmountYReceived: SmartRouteResponse, + sqrtPrice: bigint, poolKey: PoolKey, - pool: Pool, lowerTick: number, upperTick: number, - slippage: number + slippage: number, + buildZapInMessageOptions?: buildZapInMessageOptions ) => { - message.assetIn = parseAsset(tokenIn, amountIn); - message.amountToX = amountInToX.toString(); - message.amountToY = amountInToY.toString(); message.amountX = actualAmountXReceived.returnAmount; message.amountY = actualAmountYReceived.returnAmount; message.poolKey = poolKey; - message.sqrtPrice = BigInt(pool.sqrt_price); message.tickLowerIndex = lowerTick; message.tickUpperIndex = upperTick; - message.routes = generateMessageSwapOperation([actualAmountXReceived, actualAmountYReceived], slippage); + message.currentSqrtPrice = sqrtPrice; + + if (buildZapInMessageOptions) { + if (buildZapInMessageOptions.isTokenX) { + message.amountY = "0"; + } else { + message.amountX = "0"; + } + message.routes = generateMessageSwapOperation([actualAmountXReceived], slippage); + } else { + message.routes = generateMessageSwapOperation([actualAmountXReceived, actualAmountYReceived], slippage); + } + + calculateSwapFee(message); + + calculateMinimumLiquidity( + message, + actualAmountXReceived, + actualAmountYReceived, + lowerTick, + upperTick, + sqrtPrice, + slippage, + buildZapInMessageOptions + ); }; export const calculateSwapFee = (message: ZapInLiquidityResponse) => { @@ -538,8 +560,18 @@ export const calculateMinimumLiquidity = ( lowerTick: number, upperTick: number, sqrtPrice: bigint, - slippage: number + slippage: number, + buildZapInMessageOptions?: buildZapInMessageOptions ) => { + if (buildZapInMessageOptions) { + const res = buildZapInMessageOptions.isTokenX + ? getLiquidityByX(BigInt(actualAmountXReceived.returnAmount), lowerTick, upperTick, sqrtPrice, true) + : getLiquidityByY(BigInt(actualAmountYReceived.returnAmount), lowerTick, upperTick, sqrtPrice, true); + const slippageMultiplier = BigInt(Math.floor((100 - slippage) * 1000)); + message.minimumLiquidity = res.l ? (BigInt(res.l) * slippageMultiplier) / 100_000n : 0n; + return; + } + const res1 = getLiquidityByX(BigInt(actualAmountXReceived.returnAmount), lowerTick, upperTick, sqrtPrice, true); const res2 = getLiquidityByY(BigInt(actualAmountYReceived.returnAmount), lowerTick, upperTick, sqrtPrice, true); message.minimumLiquidity = diff --git a/packages/oraiswap-v3/src/main.ts b/packages/oraiswap-v3/src/main.ts index 267d3b92..501342e4 100644 --- a/packages/oraiswap-v3/src/main.ts +++ b/packages/oraiswap-v3/src/main.ts @@ -1,26 +1,20 @@ import { AMM_V3_CONTRACT, + KWT_CONTRACT, MULTICALL_CONTRACT, oraichainTokens, ORAIX_CONTRACT, + OSMO, + OSMOSIS_ORAICHAIN_DENOM, TokenItemType, USDT_CONTRACT } from "@oraichain/oraidex-common"; import { ZapConsumer } from "./zap-consumer"; import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate"; -import { extractAddress, parsePoolKey, poolKeyToString } from "./helpers"; +import { extractAddress, parsePoolKey } from "./helpers"; +import { getTickAtSqrtPrice } from "./wasm/oraiswap_v3_wasm"; async function main() { - const poolList = [ - // `orai-${USDT_CONTRACT}-3000000000-100`, - // `${ATOM_ORAICHAIN_DENOM}-orai-3000000000-100`, - // `${USDT_CONTRACT}-${USDC_CONTRACT}-500000000-10`, - // `orai-${USDC_CONTRACT}-3000000000-100`, - // `${OSMOSIS_ORAICHAIN_DENOM}-orai-3000000000-100`, - `orai-${ORAIX_CONTRACT}-3000000000-100` - ]; - const tokenIn = oraichainTokens.find((t) => extractAddress(t) === USDT_CONTRACT) as TokenItemType; - const zapper = new ZapConsumer({ routerApi: "https://osor.oraidex.io/smart-router/alpha-router", client: await CosmWasmClient.connect("https://rpc.orai.io"), @@ -29,44 +23,45 @@ async function main() { deviation: 0, smartRouteConfig: { swapOptions: { - protocols: ["OraidexV3"], + protocols: ["OraidexV3"] } - }, + } }); - // const handler = zapper.handler; + const tokenIn = oraichainTokens.find((t) => t.name === "USDT") as TokenItemType; + const pool = `${OSMOSIS_ORAICHAIN_DENOM}-orai-${(0.3 / 100) * 10 ** 12}-100`; + const poolKey = parsePoolKey(pool); - // const pool = await handler.getPool(parsePoolKey(poolList[0])); - // console.log(pool); - // const tickMap = await zapper.getFullTickmap(parsePoolKey(poolList[0])); - // console.log(tickMap); + // for (let i = 0; i < 10; i++) { + // const poolInfo = await zapper.handler.getPool(poolKey); + // console.log("poolInfo", poolInfo); + // } - //[221800, 2900, 700, 800, 1000, 1800, -5500, -4500, -221800] - //[221800, 2900, 700, 800, 1000, 1800, -5500, -4500, -221800] + const tickSpacing = poolKey.fee_tier.tick_spacing; + const currentTick = (await zapper.handler.getPool(poolKey)).pool.current_tick_index; - // const minTick = getMinTick(poolKey.fee_tier.tick_spacing); - // const maxTick = getMaxTick(poolKey.fee_tier.tick_spacing); - // const tickMap = await handler.tickMap(parsePoolKey(poolList[0]), minTick, maxTick, true); - // console.log(tickMap); - // const tickMap2 = await zapper.getFullTickmap(parsePoolKey(poolList[0])); - // console.log({ tickMap2 }); + // console.log(getTickAtSqrtPrice(314557996917228655710133n, 10)); - // const liquidityTick = await zapper.getAllLiquidityTicks(parsePoolKey(poolList[0]), tickMap2); - // console.log({ liquidityTick }); - const tickSpacing = parsePoolKey(poolList[0]).fee_tier.tick_spacing; - const currentTick = (await zapper.handler.getPool(parsePoolKey(poolList[0]))).pool.current_tick_index; + // console.time("processZapInPositionLiquidity"); + // const res = await zapper.processZapInPositionLiquidity({ + // poolKey: poolKey, + // tokenIn: tokenIn as TokenItemType, + // amountIn: "1000000000", + // lowerTick: currentTick - tickSpacing * 1, + // upperTick: currentTick + tickSpacing * 1, + // tokenX: oraichainTokens.find((t) => extractAddress(t) === poolKey.token_x) as TokenItemType, + // tokenY: oraichainTokens.find((t) => extractAddress(t) === poolKey.token_y) as TokenItemType, + // }); + // console.timeEnd("processZapInPositionLiquidity"); - const poolKey = parsePoolKey(poolList[0]); - const res = await zapper.processZapInPositionLiquidity({ - poolKey: poolKey, - tokenIn: tokenIn as TokenItemType, - amountIn: "1000000000", - lowerTick: currentTick - tickSpacing * 3, - upperTick: currentTick + tickSpacing * 3, - tokenX: oraichainTokens.find((t) => extractAddress(t) === 'orai') as TokenItemType, - tokenY: oraichainTokens.find((t) => extractAddress(t) === ORAIX_CONTRACT) as TokenItemType, + const res = await zapper.processZapOutPositionLiquidity({ + owner: "orai1hvr9d72r5um9lvt0rpkd4r75vrsqtw6yujhqs2", + tokenId: 4275, + tokenOut: tokenIn, + zapFee: 0, }); - console.log({ res }); + console.dir(res, { depth: null }); + // console.dir(res, { depth: null }); } main().catch(console.error); diff --git a/packages/oraiswap-v3/src/types.ts b/packages/oraiswap-v3/src/types.ts index 2fa4655a..81063b2f 100644 --- a/packages/oraiswap-v3/src/types.ts +++ b/packages/oraiswap-v3/src/types.ts @@ -239,31 +239,18 @@ export type SmartRouteConfig = { }; }; -export type SmartRouteReponse = { - swapAmount: string; - returnAmount: string; - routes: any[]; -}; - export type ZapInLiquidityResponse = { - minimumLiquidity?: Liquidity; - routes: Route[]; - - amountToX: Uint128; - amountToY: Uint128; - assetIn: Asset; - minimumReceiveX?: Uint128; - minimumReceiveY?: Uint128; + status: ZapInResult; poolKey: PoolKey; - tickLowerIndex: number; - tickUpperIndex: number; amountX: Uint128; amountY: Uint128; - sqrtPrice: bigint; - currentTick: number; - swapFee: number; - result: ZapInResult; + routes: Route[]; + tickLowerIndex: number; + tickUpperIndex: number; + minimumLiquidity?: Liquidity; + currentTick: number; + currentSqrtPrice: SqrtPrice; }; export type ZapOutLiquidityResponse = { @@ -279,27 +266,25 @@ export type ZapOutLiquidityResponse = { result: ZapOutResult; }; -export enum ZapInResult { - // Error - NoRouteFound = "No route found to zap", - SomethingWentWrong = "Something went wrong", +export type RouteParams = { + sourceAsset: TokenItemType; + destAsset: TokenItemType; + amount: bigint; +}; +export enum ZapInResult { // in range - InRangeNoRouteThroughSelf = "This zap operation has no swap through this pool and the position is in range so the accurancy is good", - InRangeHasRouteThroughSelf = "This zap operation has swap through this pool and the position is in range so the accurancy is good", - InRangeHasRouteThroughSelfMayBecomeOutRange = "This zap operation has swap through this pool and the position is in range but the next tick is out of range so the accurancy is low", + InRangeNoRouteThroughSelf = "This zap operation has no swap through this pool and the position is in range so the accuracy is good", + InRangeHasRouteThroughSelf = "This zap operation has swap through this pool and the position is in range so the accuracy is good", + InRangeHasRouteThroughSelfMayBecomeOutRange = "This zap operation has swap through this pool and the position is in range but the next tick is out of range so the accuracy is low", // out range - OutRangeNoRouteThroughSelf = "This zap operation has no swap through this pool and the position is out of range so the accurancy is good", - OutRangeHasRouteThroughSelf = "This zap operation has swap through this pool and the position is out of range but the next tick is not in range so the accurancy is good", - OutRangeHasRouteThroughSelfMayBecomeInRange = "This zap operation has swap through this pool and the position is out of range but the next tick is in range so the accurancy is low" + OutRangeNoRouteThroughSelf = "This zap operation has no swap through this pool and the position is out of range so the accuracy is good", + OutRangeHasRouteThroughSelf = "This zap operation has swap through this pool and the position is out of range but the next tick is not in range so the accuracy is good", + OutRangeHasRouteThroughSelfMayBecomeInRange = "This zap operation has swap through this pool and the position is out of range but the next tick is in range so the accuracy is low" } export enum ZapOutResult { - // Error - NoRouteFound = "No route found to zap", - SomethingWentWrong = "Something went wrong", - // Success Success = "Zap out successfully" } diff --git a/packages/oraiswap-v3/src/zap-consumer.ts b/packages/oraiswap-v3/src/zap-consumer.ts index 97281fde..e29223b8 100644 --- a/packages/oraiswap-v3/src/zap-consumer.ts +++ b/packages/oraiswap-v3/src/zap-consumer.ts @@ -1,13 +1,12 @@ -import { BigDecimal, oraichainTokens, parseAssetInfo, TokenItemType } from "@oraichain/oraidex-common"; +import { BigDecimal, oraichainTokens, TokenItemType } from "@oraichain/oraidex-common"; import { OraiswapV3Handler } from "./handler"; import { ActionRoute, CalculateSwapResult, - LiquidityTick, PoolKey, + RouteParams, SmartRouteConfig, SmartRouteResponse, - Tickmap, ZapConfig, ZapInLiquidityResponse, ZapInResult, @@ -16,7 +15,6 @@ import { } from "./types"; import { getLiquidityByX, - getLiquidityByY, getMaxSqrtPrice, getMinSqrtPrice, getTickAtSqrtPrice, @@ -24,26 +22,35 @@ import { } from "./wasm/oraiswap_v3_wasm"; import { buildZapOutMessage, - calculateMinimumLiquidity, calculateRewardAmounts, - calculateSwapFee, - calculateTokenAmounts, extractAddress, extractOraidexV3Actions, - generateMessageSwapOperation, - getFeeRate, - parseAsset, poolKeyToString, populateMessageZapIn, shiftDecimal } from "./helpers"; -import { Pool } from "@oraichain/oraidex-contracts-sdk/build/OraiswapV3.types"; -import { RouteNotFoundError } from "./error"; +import { Pool, PoolWithPoolKey } from "@oraichain/oraidex-contracts-sdk/build/OraiswapV3.types"; +import { RouteNoLiquidity, RouteNotFoundError, SpamTooManyRequestsError } from "./error"; -/** Read the flow chart below to understand the process of the ZapConsumer +/** Read the flow chart below to understand the process of the ZapConsumer class easier Flow ref: https://lucid.app/lucidchart/11f8ee36-ee71-4f46-8028-a953ac4f5e87/edit?viewport_loc=-3263%2C-2130%2C3114%2C1694%2C0_0&invitationId=inv_cbe9b842-b255-4c8a-824e-2bc78a6f3860 */ +/** + * The ZapConsumer class is responsible for consuming the smart router API to find the best route for swapping tokens and provide/remove liquidity for specific pools & range. + * It also simulates the swap off-chain to calculate the expected result of the swap. + * @returns ZapConsumer + * @constructor - create a new instance of the ZapConsumer + * @param config - the configuration object for the ZapConsumer + * @function handler - get the OraiswapV3Handler instance + * @function findRoute - find the best route for swapping tokens + * @function findZapRoutes - find the best routes for swapping tokens + * @function simulateSwapOffChain - simulate the swap off-chain for the route which go through the target pool want to add liquidity + * @function simulateSwapOffChainForRoute - go through the routes, simulate the swap off-chain for the route which go through the target pool want to add liquidity + * @function processZapInWithSingleSide - process the zap in with a single side + * @function processZapInPositionLiquidity - find routes, simulate swap, simulate liquidity can be added + * @function processZapOutPositionLiquidity - find the best routes for removing liquidity + */ export class ZapConsumer { private _router: string; private _handler: OraiswapV3Handler; @@ -61,142 +68,90 @@ export class ZapConsumer { return this._handler; } - private async getPriceInfo({ - sourceAsset, - destAsset - }: { - sourceAsset: TokenItemType; - destAsset: TokenItemType; - }): Promise { - try { - if (sourceAsset.name === destAsset.name) - return { - swapAmount: (10n ** BigInt(sourceAsset.decimals)).toString(), - returnAmount: (10n ** BigInt(sourceAsset.decimals)).toString(), - routes: null - }; - const res = await fetch(this._router, { - method: "POST", - body: JSON.stringify({ - sourceAsset: extractAddress(sourceAsset), - sourceChainId: sourceAsset.chainId, - destAsset: extractAddress(destAsset), - destChainId: destAsset.chainId, - offerAmount: (10n ** BigInt(sourceAsset.decimals)).toString(), - swapOptions: this._smartRouteConfig.swapOptions - }), - headers: { - "Content-Type": "application/json" - } - }); + /** + * Get the price info of the source asset to the destination asset + * @param route - the route params + * @returns SmartRouteResponse + * @throws RouteNotFoundError + * @throws SpamTooManyRequestsError + * @throws Error + */ + private async findRoute(route: RouteParams, isGetPrice: boolean = false): Promise { + const { sourceAsset, destAsset, amount } = route; + if (sourceAsset.name === destAsset.name) { + return { swapAmount: amount.toString(), returnAmount: amount.toString(), routes: [] }; + } - return JSON.parse(await res.text()); - } catch (e) { - console.log(`[ZapConsumer] getPriceInfo error: ${e}`); - throw new RouteNotFoundError(e); + if (amount === 0n) { + return { swapAmount: "0", returnAmount: "0", routes: [] }; } - } - private async findRoute({ - sourceAsset, - destAsset, - amount - }: { - sourceAsset: TokenItemType; - destAsset: TokenItemType; - amount: bigint; - }): Promise { + const body = JSON.stringify({ + sourceAsset: extractAddress(sourceAsset), + sourceChainId: sourceAsset.chainId, + destAsset: extractAddress(destAsset), + destChainId: destAsset.chainId, + offerAmount: amount.toString(), + swapOptions: this._smartRouteConfig.swapOptions + }); + try { - if (amount === 0n) { - return { - swapAmount: "0", - returnAmount: "0", - routes: null - }; + const res = await fetch(this._router, { method: "POST", body, headers: { "Content-Type": "application/json" } }); + + // otherwise, if the res is not 200, throw an error because API calls limit is reached + if (res.status !== 200) { + throw new SpamTooManyRequestsError(); } - if (sourceAsset.name === destAsset.name) - return { - swapAmount: amount.toString(), - returnAmount: amount.toString(), - routes: null - }; - const res = await fetch(this._router, { - method: "POST", - body: JSON.stringify({ - sourceAsset: extractAddress(sourceAsset), - sourceChainId: sourceAsset.chainId, - destAsset: extractAddress(destAsset), - destChainId: destAsset.chainId, - offerAmount: amount.toString(), - swapOptions: this._smartRouteConfig.swapOptions - }), - headers: { - "Content-Type": "application/json" + + const response: SmartRouteResponse = await res.json(); + + if (response.returnAmount === "0") { + if (isGetPrice) { + // maybe the amount of source is too small, try to increase the amount + const newAmount = BigInt(amount) * 10000n; + return await this.findRoute({ sourceAsset, destAsset, amount: newAmount }, false); } - }); - return JSON.parse(await res.text()); + throw new RouteNoLiquidity(); + } + + return response; } catch (e) { - console.log(`[ZapConsumer] getPriceInfo error: ${e}`); - throw new RouteNotFoundError(e); + console.error(`[ZapConsumer] getPriceInfo error: ${e}`); + if (e instanceof SpamTooManyRequestsError || e instanceof RouteNoLiquidity) { + throw e; + } else { + throw new RouteNotFoundError(`${sourceAsset.name} -> ${destAsset.name}`); + } } } - private async findZapOutRoutes( - pool: any, - tokenOut: TokenItemType, - rewardAmounts: Record - ): Promise<{ - xRouteInfo: SmartRouteResponse; - yRouteInfo: SmartRouteResponse; - }> { - const xRouteInfoPromise = this.findRoute({ - sourceAsset: oraichainTokens.find((t) => extractAddress(t) === pool.pool_key.token_x), - destAsset: tokenOut, - amount: rewardAmounts[pool.pool_key.token_x] - }); - const yRouteInfoPromise = this.findRoute({ - sourceAsset: oraichainTokens.find((t) => extractAddress(t) === pool.pool_key.token_y), - destAsset: tokenOut, - amount: rewardAmounts[pool.pool_key.token_y] - }); - const [xRouteInfo, yRouteInfo] = await Promise.all([xRouteInfoPromise, yRouteInfoPromise]); - return { xRouteInfo, yRouteInfo }; - } - - private async findZapInRoutes( - tokenIn: TokenItemType, - tokenX: TokenItemType, - tokenY: TokenItemType, - amountInToX: bigint, - amountInToY: bigint - ) { - const xRouteInfoPromise = this.findRoute({ - sourceAsset: tokenIn, - destAsset: tokenX, - amount: amountInToX - }); - const yRouteInfoPromise = this.findRoute({ - sourceAsset: tokenIn, - destAsset: tokenY, - amount: amountInToY - }); - const [xRouteInfo, yRouteInfo] = await Promise.all([xRouteInfoPromise, yRouteInfoPromise]); - return { xRouteInfo, yRouteInfo }; + /** + * Find the best routes + * @param routeParams - the route array want to find + * @returns SmartRouteResponse[] + */ + private async findZapRoutes(routeParams: RouteParams[], isGetPrice: boolean = false): Promise { + const promises = routeParams.map((params) => this.findRoute(params, isGetPrice)); + return Promise.all(promises); } + /** + * Simulate the swap off-chain for the route which go through the target pool want to add liquidity + * @param poolKey - pool key of the pool + * @param pool - pool info + * @param route - the route want to simulate + * @returns result of the swap + */ private async simulateSwapOffChain(poolKey: PoolKey, pool: Pool, route: ActionRoute): Promise { - const isXToY = route.tokenOut === poolKey.token_x ? false : true; - const amountOut = route.tokenOutAmount; + const isXToY = route.tokenOut !== poolKey.token_x; const tickMap = await this._handler.getFullTickmap(poolKey); const liquidityTicks = await this._handler.getAllLiquidityTicks(poolKey, tickMap); - const convertLiquidityTicks = liquidityTicks.map((tick) => { - return { - ...tick, - liquidity_change: BigInt(tick.liquidity_change), - } - }); - const convertPool = { + const liquidityChanges = liquidityTicks.map((tick) => ({ + ...tick, + liquidity_change: BigInt(tick.liquidity_change) + })); + const poolInfo = { ...pool, liquidity: BigInt(pool.liquidity), sqrt_price: BigInt(pool.sqrt_price), @@ -205,31 +160,48 @@ export class ZapConsumer { fee_protocol_token_x: BigInt(pool.fee_protocol_token_x), fee_protocol_token_y: BigInt(pool.fee_protocol_token_y) }; - const swapResult = simulateSwap( + + return simulateSwap( tickMap, poolKey.fee_tier, - convertPool, - convertLiquidityTicks, + poolInfo, + liquidityChanges, isXToY, - BigInt(amountOut), + BigInt(route.tokenOutAmount), false, isXToY ? getMinSqrtPrice(poolKey.fee_tier.tick_spacing) : getMaxSqrtPrice(poolKey.fee_tier.tick_spacing) ); - return swapResult; } - private async processZapInWithSingleSide({ - poolKey, - pool, - sqrtPrice, - tokenIn, - amountIn, - lowerTick, - upperTick, - tokenX, - tokenY, - slippage = 1 - }: { + /** + * Go through the routes, simulate the swap off-chain for the route which go through the target pool want to add liquidity + * @param routes - the routes want to simulate + * @param poolKey - pool key of the pool + * @param pool - pool info + * @returns SmartRouteResponse[] + */ + private async simulateSwapOffChainForRoute(routes: ActionRoute[], poolKey: PoolKey, pool: Pool) { + for (const route of routes) { + if (route.swapInfo.find((swap) => swap.poolId === poolKeyToString(poolKey))) { + console.log(`Simulate swap off-chain for route: ${route.tokenIn} -> ${route.tokenOut}`); + const swapResult = await this.simulateSwapOffChain(poolKey, pool, route); + console.log(`Simulate swap off-chain result: ${pool.sqrt_price} target: ${swapResult.target_sqrt_price}`); + pool.sqrt_price = ((BigInt(pool.sqrt_price) + BigInt(swapResult.target_sqrt_price)) / 2n).toString(); + const tick = getTickAtSqrtPrice(BigInt(pool.sqrt_price), poolKey.fee_tier.tick_spacing); + pool.current_tick_index = tick; + + // NOTE: now simulate one time only + break; + } + } + } + + /** + * Process the zap in with a single side + * @param params - the params for providing liquidity + * @returns result of the zap in operation for single side position + */ + private async processZapInWithSingleSide(params: { poolKey: PoolKey; pool: Pool; sqrtPrice: bigint; @@ -242,6 +214,20 @@ export class ZapConsumer { slippage?: number; }): Promise { try { + const { + poolKey, + pool, + sqrtPrice, + tokenIn, + amountIn, + lowerTick, + upperTick, + tokenX, + tokenY, + slippage = 1 + } = params; + + // Get the token need to swap to let tokenNeed: TokenItemType; let isTokenX: boolean = true; if (upperTick < pool.current_tick_index) { @@ -251,6 +237,7 @@ export class ZapConsumer { tokenNeed = tokenX; } + // Get the actual receive amount const actualReceive = await this.findRoute({ sourceAsset: tokenIn, destAsset: tokenNeed, @@ -259,108 +246,81 @@ export class ZapConsumer { const routes: ActionRoute[] = extractOraidexV3Actions(actualReceive.routes); - let simulatedNextSqrtPrice = BigInt(pool.sqrt_price); - let simulateNextTick = pool.current_tick_index; - for (const route of routes) { - if (route.swapInfo.find((swap) => swap.poolId === poolKeyToString(poolKey))) { - const swapResult = await this.simulateSwapOffChain(poolKey, pool, route); - - pool.sqrt_price = ((BigInt(pool.sqrt_price) + BigInt(swapResult.target_sqrt_price)) / 2n).toString(); - const tick = getTickAtSqrtPrice(BigInt(pool.sqrt_price), poolKey.fee_tier.tick_spacing); - pool.current_tick_index = (simulateNextTick + tick) / 2; - simulatedNextSqrtPrice = BigInt(pool.sqrt_price); - simulateNextTick = pool.current_tick_index; - } - } + await this.simulateSwapOffChainForRoute(routes, poolKey, pool); let message: ZapInLiquidityResponse = {} as ZapInLiquidityResponse; - message.assetIn = parseAsset(tokenIn, amountIn); - if (isTokenX) { - message.amountToX = amountIn; - message.amountToY = "0"; - message.amountX = actualReceive.returnAmount; - message.amountY = "0"; - } else { - message.amountToX = "0"; - message.amountToY = amountIn; - message.amountX = "0"; - message.amountY = actualReceive.returnAmount; - } - message.poolKey = poolKey; - message.sqrtPrice = BigInt(pool.sqrt_price); - message.tickLowerIndex = lowerTick; - message.tickUpperIndex = upperTick; - const routesNeed = generateMessageSwapOperation([actualReceive], slippage); - message.routes = [...routesNeed]; - message.swapFee = 0; - message.routes.forEach((route) => { - route.operations.forEach((operation) => { - message.swapFee += getFeeRate(operation); - }); - }); - const res = isTokenX - ? getLiquidityByX(BigInt(actualReceive.returnAmount), lowerTick, upperTick, BigInt(pool.sqrt_price), true) - : getLiquidityByY(BigInt(actualReceive.returnAmount), lowerTick, upperTick, BigInt(pool.sqrt_price), true); - message.minimumLiquidity = res.l ? (BigInt(res.l) * BigInt(100 - slippage)) / 100n : 0n; + + populateMessageZapIn( + message, + actualReceive, + actualReceive, + BigInt(pool.sqrt_price), + poolKey, + lowerTick, + upperTick, + slippage, + { + isTokenX, + isSingleSide: true + } + ); if (sqrtPrice === BigInt(pool.sqrt_price)) { - message.result = ZapInResult.OutRangeNoRouteThroughSelf; + message.status = ZapInResult.OutRangeNoRouteThroughSelf; return message; } - if (simulateNextTick < upperTick && simulateNextTick >= lowerTick) { - message.result = ZapInResult.OutRangeHasRouteThroughSelfMayBecomeInRange; - message.currentTick = simulateNextTick; - message.sqrtPrice = simulatedNextSqrtPrice; - + if (pool.current_tick_index < upperTick && pool.current_tick_index >= lowerTick) { + message.status = ZapInResult.OutRangeHasRouteThroughSelfMayBecomeInRange; + message.currentTick = pool.current_tick_index; + message.currentSqrtPrice = BigInt(pool.sqrt_price); return message; } else { - message.result = ZapInResult.OutRangeHasRouteThroughSelf; + message.status = ZapInResult.OutRangeHasRouteThroughSelf; return message; } } catch (e) { console.log(`[ZapConsumer] processZapInWithSingleSide error: ${e}`); - const message: ZapInLiquidityResponse = {} as ZapInLiquidityResponse; - if (e instanceof RouteNotFoundError) { - message.result = ZapInResult.NoRouteFound; - } else { - message.result = ZapInResult.SomethingWentWrong; - } - return message; + throw e; } } - public async processZapInPositionLiquidity({ - poolKey, - tokenIn, - tokenX, - tokenY, - amountIn, - lowerTick, - upperTick, - slippage = 1 - }: { + /** + * Find routes, simulate swap, simulate liquidity can be added + * @param params - the params for providing liquidity + * @returns result of the zap in operation + */ + public async processZapInPositionLiquidity(params: { poolKey: PoolKey; tokenIn: TokenItemType; - tokenX: TokenItemType; - tokenY: TokenItemType; amountIn: string; lowerTick: number; upperTick: number; + tokenX: TokenItemType; + tokenY: TokenItemType; slippage?: number; }): Promise { try { - const pool = await this.handler.getPool(poolKey); - const sqrtPrice = BigInt(pool.pool.sqrt_price); - - let zapResult: ZapInResult; - let result: ZapInLiquidityResponse; - - if (lowerTick > pool.pool.current_tick_index || upperTick <= pool.pool.current_tick_index) { - result = await this.processZapInWithSingleSide({ - poolKey, + // take params + const { poolKey, tokenIn, amountIn, lowerTick, upperTick, tokenX, tokenY, slippage = 1 } = params; + const pool = await this._handler.getPool(poolKey); + console.log(`Pool ${tokenX.name}/${tokenY.name} - ${poolKey.fee_tier.fee / 10 ** 10}%`); + console.log(`Want to zap ${amountIn} ${tokenIn.name}`); + console.log(`Pool now ${pool.pool.sqrt_price} - ${pool.pool.current_tick_index}`); + + // init message response + const zapInResult: ZapInLiquidityResponse = {} as ZapInLiquidityResponse; + zapInResult.poolKey = pool.pool_key; + zapInResult.tickLowerIndex = lowerTick; + zapInResult.tickUpperIndex = upperTick; + + // if the position is out range, call @processZapInWithSingleSide + if (lowerTick >= pool.pool.current_tick_index || upperTick < pool.pool.current_tick_index) { + console.log("Position is out of range"); + const zapInSingleSideResult = await this.processZapInWithSingleSide({ + poolKey: pool.pool_key, pool: pool.pool, - sqrtPrice, + sqrtPrice: BigInt(pool.pool.sqrt_price), tokenIn, amountIn, lowerTick, @@ -369,218 +329,221 @@ export class ZapConsumer { tokenY, slippage }); - } - if (result) { - if (result.result !== ZapInResult.OutRangeHasRouteThroughSelfMayBecomeInRange) { - return result; - } else { - pool.pool.current_tick_index = result.currentTick; - pool.pool.sqrt_price = BigInt(pool.pool.sqrt_price).toString(); + if (zapInSingleSideResult.status !== ZapInResult.OutRangeHasRouteThroughSelfMayBecomeInRange) { + return zapInSingleSideResult; } + + pool.pool.current_tick_index = zapInSingleSideResult.currentTick; + pool.pool.sqrt_price = zapInSingleSideResult.currentSqrtPrice.toString(); + zapInResult.status = zapInSingleSideResult.status; } - const { amount: yPerX, l: liquidity } = getLiquidityByX( + // snap start sqrt price for checking if the pool is changed + const startSqrtPrice = BigInt(pool.pool.sqrt_price); + + // Calculate 1: based on yPerX in pool, X and Y price in tokenIn + const { amount: yPerXAmount, l: liquidity } = getLiquidityByX( 10n ** BigInt(tokenX.decimals), lowerTick, upperTick, - sqrtPrice, + BigInt(pool.pool.sqrt_price), true ); - let m3 = shiftDecimal(BigInt(yPerX.toString()), tokenY.decimals); - let m1 = new BigDecimal(1); - let m2 = new BigDecimal(1); - const getXPriceByTokenInPromise = this.getPriceInfo({ - sourceAsset: tokenX, - destAsset: tokenIn - }); - const getYPriceByTokenInPromise = this.getPriceInfo({ - sourceAsset: tokenY, - destAsset: tokenIn - }); - const [getXPriceByTokenIn, getYPriceByTokenIn] = await Promise.all([ - getXPriceByTokenInPromise, - getYPriceByTokenInPromise - ]); - if (![poolKey.token_x, poolKey.token_y].includes(extractAddress(tokenIn))) { - m1 = shiftDecimal(BigInt(getXPriceByTokenIn.returnAmount), tokenIn.decimals); - m2 = shiftDecimal(BigInt(getYPriceByTokenIn.returnAmount), tokenIn.decimals); + console.log(`[CAL1] yPerXAmount: ${yPerXAmount} - liquidity: ${liquidity}`); + let yPerX = shiftDecimal(BigInt(yPerXAmount.toString()), tokenY.decimals); + let xPriceByTokenIn = new BigDecimal(1); + let yPriceByTokenIn = new BigDecimal(1); + let [getXPriceByTokenIn, getYPriceByTokenIn] = await this.findZapRoutes( + [ + { + sourceAsset: tokenX, + destAsset: tokenIn, + amount: 10n ** BigInt(tokenX.decimals) + }, + { + sourceAsset: tokenY, + destAsset: tokenIn, + amount: 10n ** BigInt(tokenY.decimals) + } + ], + true + ); + + const extendDecimalX = getXPriceByTokenIn.swapAmount.length - 1 > tokenIn.decimals ? getXPriceByTokenIn.swapAmount.length - 1 - tokenIn.decimals : 0; + const extendDecimalY = getYPriceByTokenIn.swapAmount.length - 1 > tokenIn.decimals ? getYPriceByTokenIn.swapAmount.length - 1 - tokenIn.decimals : 0; + + if (![pool.pool_key.token_x, pool.pool_key.token_y].includes(extractAddress(tokenIn))) { + xPriceByTokenIn = shiftDecimal(BigInt(getXPriceByTokenIn.returnAmount), tokenIn.decimals + extendDecimalX); + yPriceByTokenIn = shiftDecimal(BigInt(getYPriceByTokenIn.returnAmount), tokenIn.decimals + extendDecimalY); } else { - if (extractAddress(tokenIn) === poolKey.token_x) { - m2 = shiftDecimal(BigInt(getYPriceByTokenIn.returnAmount), tokenIn.decimals); + if (extractAddress(tokenIn) === pool.pool_key.token_x) { + yPriceByTokenIn = shiftDecimal(BigInt(getYPriceByTokenIn.returnAmount), tokenIn.decimals + extendDecimalY); } else { - m1 = shiftDecimal(BigInt(getXPriceByTokenIn.returnAmount), tokenIn.decimals); + xPriceByTokenIn = shiftDecimal(BigInt(getXPriceByTokenIn.returnAmount), tokenIn.decimals + extendDecimalX); } } - let x = new BigDecimal(amountIn).div(m1.add(m2.mul(m3))); - let y = x.mul(m3); - let amountX = Math.round(x.toNumber()); - let amountY = Math.round(y.toNumber()); - let amountInToX = BigInt(Math.round(x.mul(m1).toNumber())); - let amountInToY = BigInt(amountIn) - amountInToX; - const actualAmountXReceivedPromise = this.findRoute({ - sourceAsset: tokenIn, - destAsset: tokenX, - amount: amountInToX - }); - const actualAmountYReceivedPromise = this.findRoute({ - sourceAsset: tokenIn, - destAsset: tokenY, - amount: amountInToY - }); - const [actualAmountXReceived, actualAmountYReceived] = await Promise.all([ - actualAmountXReceivedPromise, - actualAmountYReceivedPromise + let xResult = new BigDecimal(amountIn).div(xPriceByTokenIn.add(yPriceByTokenIn.mul(yPerX))); + let yResult = xResult.mul(yPerX); + let amountX = Math.round(xResult.toNumber()); + let amountY = Math.round(yResult.toNumber()); + let amountInToX = BigInt(Math.round(xResult.mul(xPriceByTokenIn).toNumber())); + let amountInToY = BigInt(amountIn) - amountInToX; + console.log(`[CAL1] Amount to X: ${amountInToX} - Amount to Y: ${amountInToY}`); + console.log(`[CAL1] Amount X: ${amountX} - Amount Y: ${amountY}`); + + // After calculate equation, we have to get the actual amount received + const [actualAmountXReceived, actualAmountYReceived] = await this.findZapRoutes([ + { + sourceAsset: tokenIn, + destAsset: tokenX, + amount: amountInToX + }, + { + sourceAsset: tokenIn, + destAsset: tokenY, + amount: amountInToY + } ]); + console.log(`[CAL2] Actual amount X received: ${actualAmountXReceived.returnAmount}`); + console.log(`[CAL2] Actual amount Y received: ${actualAmountYReceived.returnAmount}`); const routes: ActionRoute[] = extractOraidexV3Actions([ ...actualAmountXReceived.routes, ...actualAmountYReceived.routes ]); - zapResult = ZapInResult.InRangeNoRouteThroughSelf; - let simulatedNextSqrtPrice = BigInt(pool.pool.sqrt_price); - let simulatedNextTick = pool.pool.current_tick_index; - for (const route of routes) { - if (route.swapInfo.find((swap) => swap.poolId === poolKeyToString(poolKey))) { - const swapResult = await this.simulateSwapOffChain(poolKey, pool.pool, route); - - pool.pool.sqrt_price = ( - (BigInt(pool.pool.sqrt_price) + BigInt(swapResult.target_sqrt_price)) / - 2n - ).toString(); - const tick = getTickAtSqrtPrice(BigInt(pool.pool.sqrt_price), poolKey.fee_tier.tick_spacing); - pool.pool.current_tick_index = (simulatedNextTick + tick) / 2; - simulatedNextTick = pool.pool.current_tick_index; - simulatedNextSqrtPrice = BigInt(pool.pool.sqrt_price); - } - } + // if we don't have to calculate anything more, the result is InRangeNoRouteThroughSelf + zapInResult.status = ZapInResult.InRangeNoRouteThroughSelf; - let liquidityAfter = liquidity; - if (sqrtPrice !== BigInt(pool.pool.sqrt_price)) { - zapResult = ZapInResult.InRangeHasRouteThroughSelf; - if (simulatedNextTick > upperTick || simulatedNextTick < lowerTick) { - zapResult = ZapInResult.InRangeHasRouteThroughSelfMayBecomeOutRange; + // if the route go through the target pool, we have to simulate the swap off-chain for better accuracy + await this.simulateSwapOffChainForRoute(routes, pool.pool_key, pool.pool); + console.log(`[CAL3] After simulate: ${pool.pool.sqrt_price} ${pool.pool.current_tick_index}`); + + if (startSqrtPrice !== BigInt(pool.pool.sqrt_price)) { + // if the sqrt price is changed, the result is InRangeHasRouteThroughSelf + zapInResult.status = ZapInResult.InRangeHasRouteThroughSelf; + + if (pool.pool.current_tick_index >= upperTick || pool.pool.current_tick_index < lowerTick) { + console.log("Position may come out of range"); + // if the pool is out range, the result is InRangeHasRouteThroughSelfMayBecomeOutRange + zapInResult.status = ZapInResult.InRangeHasRouteThroughSelfMayBecomeOutRange; - const message: ZapInLiquidityResponse = {} as ZapInLiquidityResponse; - message.result = zapResult; populateMessageZapIn( - message, - tokenIn, - amountIn, - amountInToX, - amountInToY, - actualAmountXReceived, - actualAmountYReceived, - poolKey, - pool.pool, - lowerTick, - upperTick, - slippage - ); - calculateSwapFee(message); - calculateMinimumLiquidity( - message, + zapInResult, actualAmountXReceived, actualAmountYReceived, + BigInt(pool.pool.sqrt_price), + pool.pool_key, lowerTick, upperTick, - sqrtPrice, slippage ); - return message; + return zapInResult; } - const { amount: yPerXAfter, liquidityAfter: l } = await getLiquidityByX( + // Calculate 2: based on xPerY in pool after simulate swap, X and Y price in tokenIn + const { amount: yPerXAfter, l: liquidityAfter } = await getLiquidityByX( 10n ** BigInt(tokenX.decimals), lowerTick, upperTick, BigInt(pool.pool.sqrt_price), true ); - liquidityAfter = l; - m3 = shiftDecimal(BigInt(yPerXAfter.toString()), tokenY.decimals); - x = new BigDecimal(amountIn).div(m1.add(m2.mul(m3))); - y = x.mul(m3); - amountX = Math.round(x.toNumber()); - amountY = Math.round(y.toNumber()); - amountInToX = BigInt(Math.round(x.mul(m1).toNumber())); + console.log(`[CAL2] yPerXAfter: ${yPerXAfter} - liquidityAfter: ${liquidityAfter}`); + yPerX = shiftDecimal(BigInt(yPerXAfter.toString()), tokenY.decimals); + xResult = new BigDecimal(amountIn).div(xPriceByTokenIn.add(yPriceByTokenIn.mul(yPerX))); + yResult = xResult.mul(yPerX); + amountX = Math.round(xResult.toNumber()); + amountY = Math.round(yResult.toNumber()); + amountInToX = BigInt(Math.round(xResult.mul(xPriceByTokenIn).toNumber())); amountInToY = BigInt(amountIn) - amountInToX; + console.log(`[CAL2] Amount to X: ${amountInToX} - Amount to Y: ${amountInToY}`); + console.log(`[CAL2] Amount X: ${amountX} - Amount Y: ${amountY}`); } + // Calculate 3: based on actual amount received, re-balance the result const diffX = Math.abs(Number(actualAmountXReceived.returnAmount) - amountX) / amountX; const diffY = Math.abs(Number(actualAmountYReceived.returnAmount) - amountY) / amountY; if (diffX > this._deviation || diffY > this._deviation) { - const x1 = new BigDecimal(actualAmountXReceived.returnAmount); - const y1 = new BigDecimal(actualAmountYReceived.returnAmount); - const xPriceByY = await this.getPriceInfo({ - sourceAsset: tokenX, - destAsset: tokenY - }); - const m4 = shiftDecimal(BigInt(xPriceByY.returnAmount), tokenY.decimals); - const deltaX = y1.sub(m3.mul(x1)).div(m3.add(m4)); - amountInToX += BigInt(Math.round(deltaX.mul(m1).toNumber())); + const xAmount = new BigDecimal(actualAmountXReceived.returnAmount); + const yAmount = new BigDecimal(actualAmountYReceived.returnAmount); + console.log({ xAmount, yAmount }); + const xPriceByYAmount = await this.findRoute( + { + sourceAsset: tokenX, + destAsset: tokenY, + amount: 10n ** BigInt(tokenX.decimals) + }, + true + ); + console.log(`[CAL3] xPriceByYAmount: ${xPriceByYAmount.returnAmount}`); + const extendDecimal = xPriceByYAmount.swapAmount.length - 1 > tokenY.decimals ? xPriceByYAmount.swapAmount.length - 1 - tokenY.decimals : 0; + const xPriceByY = shiftDecimal(BigInt(xPriceByYAmount.returnAmount), tokenY.decimals + extendDecimal); + console.log(`[CAL3] xPriceByY: ${xPriceByY}`); + const deltaX = yAmount.sub(yPerX.mul(xAmount)).div(yPerX.add(xPriceByY)); + console.log(`[CAL3] deltaX: ${deltaX}`); + amountInToX += BigInt(Math.round(deltaX.mul(xPriceByTokenIn).toNumber())); amountInToY = BigInt(amountIn) - amountInToX; } - - const { xRouteInfo, yRouteInfo } = await this.findZapInRoutes(tokenIn, tokenX, tokenY, amountInToX, amountInToY); - - const messages: ZapInLiquidityResponse = {} as ZapInLiquidityResponse; - messages.result = result ? result.result : zapResult; + console.log(`[CAL3] Amount to X: ${amountInToX} - Amount to Y: ${amountInToY}`); + + const [xRouteInfo, yRouteInfo] = await this.findZapRoutes([ + { + sourceAsset: tokenIn, + destAsset: tokenX, + amount: amountInToX + }, + { + sourceAsset: tokenIn, + destAsset: tokenY, + amount: amountInToY + } + ]); + console.log(`[CAL3] Actual amount X received: ${xRouteInfo.returnAmount}`); + console.log(`[CAL3] Actual amount Y received: ${yRouteInfo.returnAmount}`); populateMessageZapIn( - messages, - tokenIn, - amountIn, - amountInToX, - amountInToY, - xRouteInfo, - yRouteInfo, - poolKey, - pool.pool, - lowerTick, - upperTick, - slippage - ); - calculateSwapFee(messages); - calculateMinimumLiquidity( - messages, + zapInResult, xRouteInfo, yRouteInfo, + BigInt(pool.pool.sqrt_price), + pool.pool_key, lowerTick, upperTick, - BigInt(pool.pool.sqrt_price), slippage ); - return messages; + return zapInResult; } catch (e) { console.log(`[ZapConsumer] processZapInPositionLiquidity error: ${e}`); - const message: ZapInLiquidityResponse = {} as ZapInLiquidityResponse; - if (e instanceof RouteNotFoundError) { - message.result = ZapInResult.NoRouteFound; - } else { - message.result = ZapInResult.SomethingWentWrong; - } - return message; + throw e; } } + /** + * Find the best routes for removing liquidity + * @param tokenId - the token id of the position + * @param owner - the owner of the position + * @param tokenOut - the token out + * @param zapFee - the zap fee + * @param slippage - the slippage + * @returns result of the zap out operation + */ public async processZapOutPositionLiquidity({ tokenId, owner, tokenOut, - slippage = 1, - zapFee + zapFee, + slippage = 1 }: { tokenId: number; owner: string; tokenOut: TokenItemType; - slippage?: number; zapFee: number; + slippage?: number; }): Promise { try { - const rewardAmounts: Record = {}; const positions = await this._handler.getPositions(owner); let index = 0; const position = positions.find((p, i) => { @@ -590,20 +553,30 @@ export class ZapConsumer { const pool = await this._handler.getPool(position.pool_key); const { amountX, amountY } = calculateRewardAmounts(pool, position, zapFee); - rewardAmounts[pool.pool_key.token_x] = amountX; - rewardAmounts[pool.pool_key.token_y] = amountY; + const tokenX = oraichainTokens.find((t) => extractAddress(t) === pool.pool_key.token_x) as TokenItemType; + const tokenY = oraichainTokens.find((t) => extractAddress(t) === pool.pool_key.token_y) as TokenItemType; - const { xRouteInfo, yRouteInfo } = await this.findZapOutRoutes(pool, tokenOut, rewardAmounts); + if (!tokenX || !tokenY) { + throw new Error("Token X or Token Y not found in oraichainTokens."); + } + + const [xRouteInfo, yRouteInfo] = await this.findZapRoutes([ + { + sourceAsset: tokenX, + destAsset: tokenOut, + amount: amountX + }, + { + sourceAsset: tokenY, + destAsset: tokenOut, + amount: amountY + } + ]); return buildZapOutMessage(ZapOutResult.Success, index, xRouteInfo, yRouteInfo, slippage); } catch (e) { - const message: ZapOutLiquidityResponse = {} as ZapOutLiquidityResponse; - console.log(`error: ${e}`); - if (e instanceof RouteNotFoundError) { - message.result = ZapOutResult.NoRouteFound; - } else { - message.result = ZapOutResult.SomethingWentWrong; - } + console.log(`[ZapConsumer] ZapOut error: ${e}`); + throw e; } } } diff --git a/packages/oraiswap-v3/tests/data/incentives-fund-manager.wasm b/packages/oraiswap-v3/tests/data/incentives-fund-manager.wasm new file mode 100644 index 00000000..1cff07f8 Binary files /dev/null and b/packages/oraiswap-v3/tests/data/incentives-fund-manager.wasm differ diff --git a/packages/oraiswap-v3/tests/data/oraiswap-factory.wasm b/packages/oraiswap-v3/tests/data/oraiswap-factory.wasm new file mode 100644 index 00000000..8a572311 Binary files /dev/null and b/packages/oraiswap-v3/tests/data/oraiswap-factory.wasm differ diff --git a/packages/oraiswap-v3/tests/data/oraiswap-mixed-router.wasm b/packages/oraiswap-v3/tests/data/oraiswap-mixed-router.wasm new file mode 100644 index 00000000..18fd2ae7 Binary files /dev/null and b/packages/oraiswap-v3/tests/data/oraiswap-mixed-router.wasm differ diff --git a/packages/oraiswap-v3/tests/data/oraiswap-oracle.wasm b/packages/oraiswap-v3/tests/data/oraiswap-oracle.wasm new file mode 100644 index 00000000..0e535645 Binary files /dev/null and b/packages/oraiswap-v3/tests/data/oraiswap-oracle.wasm differ diff --git a/packages/oraiswap-v3/tests/data/oraiswap-pair.wasm b/packages/oraiswap-v3/tests/data/oraiswap-pair.wasm new file mode 100644 index 00000000..5c691280 Binary files /dev/null and b/packages/oraiswap-v3/tests/data/oraiswap-pair.wasm differ diff --git a/packages/oraiswap-v3/tests/data/oraiswap-v3.wasm b/packages/oraiswap-v3/tests/data/oraiswap-v3.wasm index a7d213cb..9b18f4ed 100644 Binary files a/packages/oraiswap-v3/tests/data/oraiswap-v3.wasm and b/packages/oraiswap-v3/tests/data/oraiswap-v3.wasm differ diff --git a/packages/oraiswap-v3/tests/data/zapper.wasm b/packages/oraiswap-v3/tests/data/zapper.wasm new file mode 100644 index 00000000..9d52f161 Binary files /dev/null and b/packages/oraiswap-v3/tests/data/zapper.wasm differ diff --git a/packages/oraiswap-v3/tests/handler.spec.ts b/packages/oraiswap-v3/tests/handler.spec.ts index 9338c7f0..e387f046 100644 --- a/packages/oraiswap-v3/tests/handler.spec.ts +++ b/packages/oraiswap-v3/tests/handler.spec.ts @@ -1,6 +1,12 @@ import { OraiswapV3Client, OraiswapV3Types } from "@oraichain/oraidex-contracts-sdk"; -import { calculateSqrtPrice, getGlobalMinSqrtPrice, newFeeTier, newPoolKey, toPercentage } from "../src/wasm/oraiswap_v3_wasm"; -import { bobAddress, client, createTokens, senderAddress } from "./test-common"; +import { + calculateSqrtPrice, + getGlobalMinSqrtPrice, + newFeeTier, + newPoolKey, + toPercentage +} from "../src/wasm/oraiswap_v3_wasm"; +import { bobAddress, client, createTokens, deployIncentivesFundManager, senderAddress } from "./test-common"; import fs from "fs"; import path from "path"; import { OraiswapV3Handler, poolKeyToString } from "../src"; @@ -37,6 +43,11 @@ describe("test oraiswap-v3 handler functions", () => { "auto" ); + const fundManager = await deployIncentivesFundManager({ + oraiswap_v3: "", + owner: senderAddress + }); + dex = new OraiswapV3Client( client, senderAddress, @@ -44,7 +55,7 @@ describe("test oraiswap-v3 handler functions", () => { await client.instantiate( senderAddress, dexCodeId, - { protocol_fee } as OraiswapV3Types.InstantiateMsg, + { protocol_fee, incentives_fund_manager: fundManager.contractAddress } as OraiswapV3Types.InstantiateMsg, "oraiswap_v3", "auto" ) @@ -176,9 +187,9 @@ describe("test oraiswap-v3 handler functions", () => { rewardPerSec: "1", rewardToken: { token: { - contract_addr: tokenZ.contractAddress, + contract_addr: tokenZ.contractAddress } - }, + } }); }); @@ -293,8 +304,8 @@ describe("test oraiswap-v3 handler functions", () => { const pool = poolList[0]; await dex.approve({ spender: bobAddress, - tokenId: 1, - }) + tokenId: 1 + }); const approveForAll = await handler.approveForAll(senderAddress, true); expect(approveForAll).toBeDefined(); }); diff --git a/packages/oraiswap-v3/tests/test-common.ts b/packages/oraiswap-v3/tests/test-common.ts index dc31d513..c7ff1b78 100644 --- a/packages/oraiswap-v3/tests/test-common.ts +++ b/packages/oraiswap-v3/tests/test-common.ts @@ -1,17 +1,21 @@ import { SimulateCosmWasmClient } from "@oraichain/cw-simulate"; -import { OraiswapTokenClient, OraiswapTokenTypes } from "@oraichain/oraidex-contracts-sdk"; +import { IncentivesFundManagerClient, IncentivesFundManagerTypes, OraiswapFactoryClient, OraiswapFactoryTypes, OraiswapMixedRouterClient, OraiswapMixedRouterTypes, OraiswapOracleClient, OraiswapOracleTypes, OraiswapTokenClient, OraiswapTokenTypes, OraiswapV3Client, OraiswapV3Types, ZapperClient, ZapperTypes } from "@oraichain/oraidex-contracts-sdk"; import * as oraidexArtifacts from "@oraichain/oraidex-contracts-build"; +import path from "path"; +import fs from "fs"; +import { MulticallQueryClient } from "@oraichain/common-contracts-sdk"; export const senderAddress = "orai1g4h64yjt0fvzv5v2j8tyfnpe5kmnetejvfgs7g"; export const bobAddress = "orai1602dkqjvh4s7ryajnz2uwhr8vetrwr8nekpxv5"; +export const deployer = "orai1swus8mwu8xjulawqxdwh8hvg4gknh2c64tuc0k"; + export const client = new SimulateCosmWasmClient({ chainId: "Oraichain", bech32Prefix: "orai" }); export const createTokens = async (amount: string, receiver: string, ...symbols: string[]) => { - // init airi token const tokens = await Promise.all( symbols.map(async (symbol) => { const res = await oraidexArtifacts.deployContract( @@ -35,3 +39,77 @@ export const createTokens = async (amount: string, receiver: string, ...symbols: return tokens.sort((a, b) => a.contractAddress.localeCompare(b.contractAddress)); }; + +export const deployContract = async (name: string, initMsg: any) => { + const { codeId } = await client.upload( + deployer, + fs.readFileSync(path.resolve(__dirname, "data", `${name}.wasm`)), + "auto" + ); + + const { contractAddress } = await client.instantiate( + deployer, + codeId, + initMsg, + name, + "auto" + ); + + return contractAddress; +}; + +export const createTokenWithDecimal = async (symbol: string, decimals: number) => { + const res = await oraidexArtifacts.deployContract( + client, + deployer, + { + mint: { + minter: deployer + }, + decimals, + symbol, + name: symbol, + initial_balances: [] + } as OraiswapTokenTypes.InstantiateMsg, + "token", + "oraiswap-token" + ); + + return new OraiswapTokenClient(client, deployer, res.contractAddress); +} + +export const deployMultiCall = async () => { + const res = await deployContract("multicall", {}); + return new MulticallQueryClient(client, res); +} + +export const deployIncentivesFundManager = async (msg: IncentivesFundManagerTypes.InstantiateMsg) => { + const res = await deployContract("incentives-fund-manager", msg); + return new IncentivesFundManagerClient(client, deployer, res); +}; + +export const deployDexV3 = async (msg: OraiswapV3Types.InstantiateMsg) => { + const res = await deployContract("oraiswap-v3", msg); + return new OraiswapV3Client(client, deployer, res); +} + +export const deployOracle = async (msg: OraiswapOracleTypes.InstantiateMsg) => { + const res = await deployContract("oraiswap-oracle", msg); + return new OraiswapOracleClient(client, deployer, res); +} + +export const deployFactory = async (msg: OraiswapFactoryTypes.InstantiateMsg) => { + const res = await deployContract("oraiswap-factory", msg); + return new OraiswapFactoryClient(client, deployer, res); +} + +export const deployMixedRouter = async (msg: OraiswapMixedRouterTypes.InstantiateMsg) => { + const res = await deployContract("oraiswap-mixed-router", msg); + return new OraiswapMixedRouterClient(client, deployer, res); +} + +export const deployZapper = async (msg: ZapperTypes.InstantiateMsg) => { + const res = await deployContract("zapper", msg); + return new ZapperClient(client, deployer, res); +} + diff --git a/packages/oraiswap-v3/tests/wasm-bindgen.spec.ts b/packages/oraiswap-v3/tests/wasm-bindgen.spec.ts new file mode 100644 index 00000000..0b228073 --- /dev/null +++ b/packages/oraiswap-v3/tests/wasm-bindgen.spec.ts @@ -0,0 +1,548 @@ +import { + calculateFee, + calculateMaxLiquidityPerTick, + calculateMinAmountOut, + checkTick, + checkTicks, + checkTickToSqrtPriceRelationship, + computeSwapStep, + getDeltaX, + getDeltaY, + getGlobalMaxSqrtPrice, + getLiquidityByX, + getLiquidityByY, + getMaxChunk, + getMaxPoolKeysReturned, + getMaxPoolPairsReturned, + getMaxSqrtPrice, + getMaxTick, + getMaxTickCross, + getNextSqrtPriceFromInput, + getNextSqrtPriceFromOutput, + getNextSqrtPriceXUp, + getNextSqrtPriceYDown, + getTickAtSqrtPrice, + getTickSearchRange, + isEnoughAmountToChangePrice, + isTokenX, + positionToTick +} from "../src"; +import { describe, it, expect } from "vitest"; + +describe("wasm bind gen tests", () => { + it.each< + [ + { + currentSqrtPrice: bigint; + targetSqrtPrice: bigint; + liquidity: bigint; + amount: bigint; + byAmountIn: boolean; + fee: bigint; + }, + { + next_sqrt_price: bigint; + amount_in: bigint; + amount_out: bigint; + fee_amount: bigint; + } + ] + >([ + [ + { + currentSqrtPrice: 10n ** 24n, + targetSqrtPrice: 1004987562112089027021926n, + liquidity: 2000n * 10n ** 6n, + amount: 1n, + byAmountIn: true, + fee: 6n * 10n ** 4n + }, + { + next_sqrt_price: 10n ** 24n, + amount_in: 0n, + amount_out: 0n, + fee_amount: 1n + } + ], + [ + { + currentSqrtPrice: 10n ** 24n, + targetSqrtPrice: 1004987562112089027021926n, + liquidity: 2000n * 10n ** 6n, + amount: 20n, + byAmountIn: true, + fee: 6n * 10n ** 4n + }, + { + next_sqrt_price: 1004987562112089027021926n, + amount_in: 10n, + amount_out: 9n, + fee_amount: 1n + } + ], + [ + { + currentSqrtPrice: 10n ** 24n, + targetSqrtPrice: 1004987562112089027021926n, + liquidity: 2000n * 10n ** 6n, + amount: 20n, + byAmountIn: false, + fee: 6n * 10n ** 4n + }, + { + next_sqrt_price: 1004987562112089027021926n, + amount_in: 10n, + amount_out: 9n, + fee_amount: 1n + } + ] + ])("compute swap step", (input, output) => { + const res = computeSwapStep( + input.currentSqrtPrice, + input.targetSqrtPrice, + input.liquidity, + input.amount, + input.byAmountIn, + input.fee + ); + expect(res).toMatchObject(output); + }); + + it.each< + [ + { + sqrtPriceA: bigint; + sqrtPriceB: bigint; + liquidity: bigint; + roundUp: boolean; + }, + bigint + ] + >([ + [ + { + sqrtPriceA: 10n ** 24n, + sqrtPriceB: 10n ** 24n, + liquidity: 0n, + roundUp: false + }, + 0n + ], + [ + { + sqrtPriceA: 10n ** 24n, + sqrtPriceB: 2n * 10n ** 24n, + liquidity: 2n * 10n ** 6n, + roundUp: false + }, + 1n + ] + ])("get delta x", (input, output) => { + const res = getDeltaX(input.sqrtPriceA, input.sqrtPriceB, input.liquidity, input.roundUp); + expect(res).toEqual(output); + }); + + it.each< + [ + { + sqrtPriceA: bigint; + sqrtPriceB: bigint; + liquidity: bigint; + roundUp: boolean; + }, + bigint + ] + >([ + [ + { + sqrtPriceA: 10n ** 24n, + sqrtPriceB: 10n ** 24n, + liquidity: 0n, + roundUp: false + }, + 0n + ], + [ + { + sqrtPriceA: 10n ** 24n, + sqrtPriceB: 2n * 10n ** 24n, + liquidity: 2n * 10n ** 6n, + roundUp: false + }, + 2n + ] + ])("get delta y", (input, output) => { + const res = getDeltaY(input.sqrtPriceA, input.sqrtPriceB, input.liquidity, input.roundUp); + expect(res).toEqual(output); + }); + + it.each< + [ + { + sqrtPrice: bigint; + liquidity: bigint; + amount: bigint; + xToY: boolean; + }, + bigint + ] + >([ + [ + { + sqrtPrice: 10n ** 24n, + liquidity: 10n ** 6n, + amount: 1n, + xToY: true + }, + 5n * 10n ** 23n + ] + ])("get next sqrt price from input", (input, output) => { + const res = getNextSqrtPriceFromInput(input.sqrtPrice, input.liquidity, input.amount, input.xToY); + expect(res).toEqual(output); + }); + + it.each< + [ + { + sqrtPrice: bigint; + liquidity: bigint; + amount: bigint; + xToY: boolean; + }, + bigint + ] + >([ + [ + { + sqrtPrice: 10n ** 24n, + liquidity: 2n * 10n ** 6n, + amount: 1n, + xToY: true + }, + 5n * 10n ** 23n + ] + ])("get next sqrt price from output", (input, output) => { + const res = getNextSqrtPriceFromOutput(input.sqrtPrice, input.liquidity, input.amount, input.xToY); + expect(res).toEqual(output); + }); + + it.each< + [ + { + sqrtPrice: bigint; + liquidity: bigint; + amount: bigint; + xToY: boolean; + }, + bigint + ] + >([ + [ + { + sqrtPrice: 10n ** 24n, + liquidity: 10n ** 6n, + amount: 1n, + xToY: true + }, + 5n * 10n ** 23n + ] + ])("getNextSqrtPriceXUp", (input, output) => { + const res = getNextSqrtPriceXUp(input.sqrtPrice, input.liquidity, input.amount, input.xToY); + expect(res).toEqual(output); + }); + + it.each< + [ + { + sqrtPrice: bigint; + liquidity: bigint; + amount: bigint; + xToY: boolean; + }, + bigint + ] + >([ + [ + { + sqrtPrice: 10n ** 24n, + liquidity: 10n ** 6n, + amount: 1n, + xToY: true + }, + 2n * 10n ** 24n + ] + ])("getNextSqrtPriceYDown", (input, output) => { + const res = getNextSqrtPriceYDown(input.sqrtPrice, input.liquidity, input.amount, input.xToY); + expect(res).toEqual(output); + }); + + it.each< + [ + { + amount: bigint; + startSqrtPrice: bigint; + liquidity: bigint; + fee: bigint; + byAmountIn: boolean; + xToY: boolean; + }, + boolean + ] + >([ + [ + { + amount: 340282366920938463463374607431768211455n, + startSqrtPrice: 65535383934512647000000000000n, + liquidity: 0n, + fee: 1000000000000n, + byAmountIn: false, + xToY: false + }, + true + ] + ])("isEnoughAmountToChangePrice", (input, output) => { + const res = isEnoughAmountToChangePrice( + input.amount, + input.startSqrtPrice, + input.liquidity, + input.fee, + input.byAmountIn, + input.xToY + ); + expect(res).toEqual(output); + }); + + it.each<[number, bigint]>([[1, 767028825190275976673213928125400n]])( + "calculateMaxLiquidityPerTick", + (input, output) => { + const res = calculateMaxLiquidityPerTick(input); + expect(res).toEqual(output); + } + ); + + it.each<[number, number, number]>([[1, 2, 1]])("checkTicks", (tickLower, tickUpper, tickSpacing) => { + checkTicks(tickLower, tickUpper, tickSpacing); + }); + + it.each<[number, number]>([[1, 1]])("checkTick", (tick, tickSpacing) => { + checkTick(tick, tickSpacing); + }); + + it.each<[bigint, bigint, bigint]>([[100n, 0n, 100n]])("calculateMinAmountOut", (expected, slippage, output) => { + const res = calculateMinAmountOut(expected, slippage); + expect(res).toEqual(output); + }); + + it.each<[number, number, number, number]>([[0, 0, 1, -221818]])( + "positionToTick", + (chunk, bit, tickSpacing, output) => { + const res = positionToTick(chunk, bit, tickSpacing); + console.log(res); + expect(res).toEqual(output); + } + ); + + it("getGlobalMaxSqrtPrice", () => { + const res = getGlobalMaxSqrtPrice(); + expect(res).toEqual(getMaxSqrtPrice(1)); + }); + + it("getTickSearchRange", () => { + const res = getTickSearchRange(); + expect(res).toEqual(256); + }); + + it.each<[number]>([[1]])("getMaxChunk", (tickSpacing) => { + const res = getMaxChunk(tickSpacing); + expect(res).toEqual(6931); + }); + + it("getMaxTickCross", () => { + const res = getMaxTickCross(); + expect(res).toEqual(173); + }); + + it("getMaxPoolKeysReturned", () => { + const res = getMaxPoolKeysReturned(); + expect(res).toEqual(220); + }); + + it("getMaxPoolPairsReturned", () => { + const res = getMaxPoolPairsReturned(); + expect(res).toEqual(126); + }); + + it.each< + [ + { + lower_tick_index: number; + lower_tick_fee_growth_outside_x: bigint; + lower_tick_fee_growth_outside_y: bigint; + upper_tick_index: number; + upper_tick_fee_growth_outside_x: bigint; + upper_tick_fee_growth_outside_y: bigint; + pool_current_tick_index: number; + pool_fee_growth_global_x: bigint; + pool_fee_growth_global_y: bigint; + position_fee_growth_inside_x: bigint; + position_fee_growth_inside_y: bigint; + position_liquidity: bigint; + }, + { + x: bigint; + y: bigint; + } + ] + >([ + [ + { + lower_tick_index: -24300, + lower_tick_fee_growth_outside_x: 30566305483401951213259107n, + lower_tick_fee_growth_outside_y: 3090193022255581920240205n, + upper_tick_index: -23900, + upper_tick_fee_growth_outside_x: 0n, + upper_tick_fee_growth_outside_y: 0n, + pool_current_tick_index: -24200, + pool_fee_growth_global_x: 34015516218039756676745948n, + pool_fee_growth_global_y: 3360651078360214052633596n, + position_fee_growth_inside_x: 1856777541687032563592895n, + position_fee_growth_inside_y: 164732622916975273061067n, + position_liquidity: 7823906503624803n + }, + { + x: 1245904n, + y: 82718n + } + ] + ])("calculateFee", (input, output) => { + const res = calculateFee( + input.lower_tick_index, + input.lower_tick_fee_growth_outside_x, + input.lower_tick_fee_growth_outside_y, + input.upper_tick_index, + input.upper_tick_fee_growth_outside_x, + input.upper_tick_fee_growth_outside_y, + input.pool_current_tick_index, + input.pool_fee_growth_global_x, + input.pool_fee_growth_global_y, + input.position_fee_growth_inside_x, + input.position_fee_growth_inside_y, + input.position_liquidity + ); + expect(res).toMatchObject(output); + }); + + it.each<[string, string, boolean]>([ + ["orai1zyvk3n9r8sax4xvqph97pxuhduqqsqwq6dwzj2", "orai", false], + ["orai1zyvk3n9r8sax4xvqph97pxuhduqqsqwq6dwzj2", "factory/abc/ton", false], + ["orai", "orai1zyvk3n9r8sax4xvqph97pxuhduqqsqwq6dwzj2", true] + ])( + "isTokenX", + (tokenX, tokenY, expected) => { + const res = isTokenX(tokenX, tokenY); + expect(res).toEqual(expected); + } + ); + + it.each<[number, number, bigint, boolean]>([])( + "checkTickToSqrtPriceRelationship", + (tick, tickSpacing, sqrtPrice, expected) => { + const res = checkTickToSqrtPriceRelationship(tick, tickSpacing, sqrtPrice); + expect(res).toEqual(expected); + } + ); + + it.each< + [ + { + x: bigint; + lower_tick: number; + upper_tick: number; + current_sqrt_price: bigint; + rounding_up: boolean; + }, + { + amount: bigint; + l: bigint; + } + ] + >([ + [ + { + x: 1000000n, + lower_tick: -10, + upper_tick: 10, + current_sqrt_price: 10n ** 24n, + rounding_up: true + }, + { + amount: 1000000n, + l: 2000600039999999n + } + ] + ])("getLiquidityByX", (input, output) => { + const res = getLiquidityByX( + input.x, + input.lower_tick, + input.upper_tick, + input.current_sqrt_price, + input.rounding_up + ); + expect(res).toMatchObject(output); + }); + + it.each< + [ + { + y: bigint; + lower_tick: number; + upper_tick: number; + current_sqrt_price: bigint; + rounding_up: boolean; + }, + { + amount: bigint; + l: bigint; + } + ] + >([ + [ + { + y: 1000000n, + lower_tick: -10, + upper_tick: 10, + current_sqrt_price: 10n ** 24n, + rounding_up: true + }, + { + amount: 1000000n, + l: 2000600039969988n + } + ] + ])("getLiquidityByY", (input, output) => { + const res = getLiquidityByY( + input.y, + input.lower_tick, + input.upper_tick, + input.current_sqrt_price, + input.rounding_up + ); + expect(res).toMatchObject(output); + }); + + it.each<[bigint, number, number]>([ + [10n ** 24n, 1, 0], + [10n ** 24n, 2, 0] + ])("getTickAtSqrtPrice", (sqrtPrice, tickSpacing, output) => { + const res = getTickAtSqrtPrice(sqrtPrice, tickSpacing); + expect(res).toEqual(output); + }); + + it.each<[number, number, bigint, boolean]>([ + [1, 1, 10n ** 24n, false], + [1, 1, 10n ** 24n, false] + ])("checkTickToSqrtPriceRelationship", (tick_index, tick_spacing, sqrt_price, output) => { + const res = checkTickToSqrtPriceRelationship(tick_index, tick_spacing, sqrt_price); + expect(res).toEqual(output); + }) +}); diff --git a/packages/oraiswap-v3/tests/zap-consumer.spec.ts b/packages/oraiswap-v3/tests/zap-consumer.spec.ts index 794512b3..10dd0a4d 100644 --- a/packages/oraiswap-v3/tests/zap-consumer.spec.ts +++ b/packages/oraiswap-v3/tests/zap-consumer.spec.ts @@ -1,61 +1,287 @@ import path from "path"; import { fileURLToPath } from "url"; import { ZapConsumer } from "../src/zap-consumer"; -import { OraiswapV3Client, OraiswapV3Types } from "@oraichain/oraidex-contracts-sdk"; +import { + IncentivesFundManagerClient, + OraiswapFactoryClient, + OraiswapMixedRouterClient, + OraiswapOracleClient, + OraiswapTokenClient, + OraiswapV3Client, + OraiswapV3Types, + ZapperClient +} from "@oraichain/oraidex-contracts-sdk"; import fs from "fs"; -import { describe, it, beforeEach } from "vitest"; -import { client } from "./test-common"; +import { describe, it, beforeEach, expect } from "vitest"; +import { + client, + createTokenWithDecimal, + deployDexV3, + deployer, + deployFactory, + deployIncentivesFundManager, + deployMixedRouter, + deployMultiCall, + deployOracle, + deployZapper +} from "./test-common"; +import { + getGlobalMaxSqrtPrice, + getGlobalMinSqrtPrice, + getMinTick, + getTickAtSqrtPrice, + newPoolKey, + OraiswapV3Handler +} from "../src"; +import { MulticallQueryClient } from "@oraichain/common-contracts-sdk"; + +// move signer to dynamic signing of an object +declare module "@oraichain/oraidex-contracts-sdk" { + interface OraiswapTokenClient { + connect(signer: string): OraiswapTokenClient; + } +} +OraiswapTokenClient.prototype.connect = function (signer: string): OraiswapTokenClient { + this.sender = signer; + return this; +}; + +declare module "@oraichain/oraidex-contracts-sdk" { + interface OraiswapV3Client { + connect(signer: string): OraiswapV3Client; + } +} +OraiswapV3Client.prototype.connect = function (signer: string): OraiswapV3Client { + this.sender = signer; + return this; +}; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const admin = "orai1swus8mwu8xjulawqxdwh8hvg4gknh2c64tuc0k"; const alice = "orai1g4h64yjt0fvzv5v2j8tyfnpe5kmnetejvfgs7g"; const bob = "orai1602dkqjvh4s7ryajnz2uwhr8vetrwr8nekpxv5"; describe("ZapConsumer", () => { - // init dex - let protocolFee = 250000000000; - let dex: OraiswapV3Client; - let zapper: ZapConsumer; - - beforeEach(async () => { - const { codeId: dexCodeId } = await client.upload( - admin, - fs.readFileSync(path.resolve(__dirname, "data", "oraiswap-v3.wasm")), - "auto" - ); - - const { codeId: multicalCodeId } = await client.upload( - admin, - fs.readFileSync(path.resolve(__dirname, "data", "multicall.wasm")), - "auto" - ); - - const { contractAddress: multicallAddress } = await client.instantiate( - admin, - multicalCodeId, - {}, - "multicall", - "auto" - ); - - dex = new OraiswapV3Client( - client, - admin, - ( - await client.instantiate( - admin, - dexCodeId, - { protocol_fee: protocolFee } as OraiswapV3Types.InstantiateMsg, - "oraiswap_v3", - "auto" - ) - ).contractAddress - ); + describe("Test connect function", () => { + let tokenTest: OraiswapTokenClient; + beforeEach(async () => { + tokenTest = await createTokenWithDecimal("USDT", 6); + }); + + it("connect of cw20 function", async () => { + expect(tokenTest.sender).toEqual(deployer); + await tokenTest.mint({ + recipient: alice, + amount: "1000000000" + }); + expect((await tokenTest.balance({ address: alice })).balance).toEqual("1000000000"); + await tokenTest.connect(alice).transfer({ + recipient: bob, + amount: "1000000000" + }); + expect((await tokenTest.balance({ address: alice })).balance).toEqual("0"); + expect((await tokenTest.balance({ address: bob })).balance).toEqual("1000000000"); + }); }); - it("work", async () => { - console.log("hello"); + describe("Test zap base on return message", () => { + /* + We will simulate test cases base on return message of zap function: + - zapIn: + */ + let multiCall: MulticallQueryClient; + let oraix: OraiswapTokenClient; // orai17lgwcg4sesnnuk0r6vwtp0cpxncq28phzwj3e4 + let usdt: OraiswapTokenClient; // orai1hkkmmpf6npaxq0lmjavudlu6nux63jqxdu5fge + let usdc: OraiswapTokenClient; //orai14asjuxtfd2mkwxpzlv60fd45h0jj6y4qz70hpt + let incentivesFundManager: IncentivesFundManagerClient; + let oraiswapV3: OraiswapV3Client; + let oracle: OraiswapOracleClient; + let factory: OraiswapFactoryClient; + let mixedRouter: OraiswapMixedRouterClient; + let zapper: ZapperClient; + let zapConsumer: ZapConsumer; + let handler: OraiswapV3Handler; + let feeTier1: OraiswapV3Types.FeeTier; + let feeTier2: OraiswapV3Types.FeeTier; + + beforeEach(async () => { + oraix = await createTokenWithDecimal("ORAIX", 6); + usdt = await createTokenWithDecimal("USDT", 6); + usdc = await createTokenWithDecimal("USDC", 6); + + multiCall = await deployMultiCall(); + + incentivesFundManager = await deployIncentivesFundManager({ + owner: deployer, + oraiswap_v3: deployer + }); + + oraiswapV3 = await deployDexV3({ + protocol_fee: 0.25 * 10 ** 12, + incentives_fund_manager: incentivesFundManager.contractAddress + }); + + await incentivesFundManager.updateConfig({ + oraiswapV3: oraiswapV3.contractAddress, + owner: deployer + }); + + oracle = await deployOracle({}); + + factory = await deployFactory({ + pair_code_id: 1, + token_code_id: 1, + oracle_addr: oracle.contractAddress + }); + + mixedRouter = await deployMixedRouter({ + factory_addr: factory.contractAddress, + factory_addr_v2: factory.contractAddress, + oraiswap_v3: oraiswapV3.contractAddress + }); + + zapper = await deployZapper({ + admin: deployer, + dex_v3: oraiswapV3.contractAddress, + mixed_router: mixedRouter.contractAddress + }); + + feeTier1 = { + fee: 0.003 * 10 ** 12, + tick_spacing: 100 + }; + feeTier2 = { + fee: 0.0005 * 10 ** 12, + tick_spacing: 10 + }; + + await oraiswapV3.connect(deployer).addFeeTier({ + feeTier: feeTier1 + }); + await oraiswapV3.connect(deployer).addFeeTier({ + feeTier: feeTier2 + }); + + // pool 0 + await oraiswapV3.connect(alice).createPool({ + token0: oraix.contractAddress, + token1: usdt.contractAddress, + initSqrtPrice: (2n * 10n ** 24n).toString(), + initTick: 13800, + feeTier: { + fee: 0.003 * 10 ** 12, + tick_spacing: 100 + } + }); + // pool 1 + await oraiswapV3.connect(alice).createPool({ + token0: oraix.contractAddress, + token1: usdc.contractAddress, + initSqrtPrice: (5n * 10n ** 23n).toString(), + initTick: -13900, + feeTier: { + fee: 0.003 * 10 ** 12, + tick_spacing: 100 + } + }); + // pool 2 + await oraiswapV3.connect(alice).createPool({ + token0: usdt.contractAddress, + token1: usdc.contractAddress, + initSqrtPrice: (1n * 10n ** 24n).toString(), + initTick: 0, + feeTier: { + fee: 0.0005 * 10 ** 12, + tick_spacing: 10 + } + }); + + zapConsumer = new ZapConsumer({ + client: client, + dexV3Address: oraiswapV3.contractAddress, + deviation: 0, + multiCallAddress: multiCall.contractAddress, + routerApi: "mockAPI", + smartRouteConfig: { + swapOptions: { + protocols: ["OraidexV3"] + } + } + }); + + handler = zapConsumer.handler; + }); + + it("InRangeNoRouteThroughSelf", async () => { + // zap to pool usdt usdc, use oraix + const pools = await handler.getPools(); + + await oraix.connect(deployer).mint({ + recipient: alice, + amount: (1000n * 10n ** 6n).toString() + }); + await usdc.connect(deployer).mint({ + recipient: alice, + amount: (1000n * 10n ** 6n).toString() + }); + await usdt.connect(deployer).mint({ + recipient: alice, + amount: (1000n * 10n ** 6n).toString() + }); + + await oraix.connect(alice).increaseAllowance({ + spender: oraiswapV3.contractAddress, + amount: (1000n * 10n ** 6n).toString() + }); + await usdc.connect(alice).increaseAllowance({ + spender: oraiswapV3.contractAddress, + amount: (1000n * 10n ** 6n).toString() + }); + await usdt.connect(alice).increaseAllowance({ + spender: oraiswapV3.contractAddress, + amount: (1000n * 10n ** 6n).toString() + }); + + const oraixUsdtPK = newPoolKey(oraix.contractAddress, usdt.contractAddress, feeTier1); // oraix < usdt + const usdcOraixPK = newPoolKey(oraix.contractAddress, usdc.contractAddress, feeTier1); // usdc < oraix + const usdcUsdtPK = newPoolKey(usdt.contractAddress, usdc.contractAddress, feeTier2); // usdc < usdt + + await oraiswapV3.connect(alice).createPosition({ + poolKey: oraixUsdtPK, + lowerTick: 13700, + upperTick: 13900, + liquidityDelta: (10000n * 10n ** 6n).toString(), + slippageLimitLower: getGlobalMinSqrtPrice().toString(), + slippageLimitUpper: getGlobalMaxSqrtPrice().toString() + }); + await oraiswapV3.connect(alice).createPosition({ + poolKey: usdcOraixPK, + lowerTick: -14000, + upperTick: -13800, + liquidityDelta: (10000n * 10n ** 6n).toString(), + slippageLimitLower: getGlobalMinSqrtPrice().toString(), + slippageLimitUpper: getGlobalMaxSqrtPrice().toString() + }); + await oraiswapV3.connect(alice).createPosition({ + poolKey: usdcUsdtPK, + lowerTick: -10, + upperTick: 10, + liquidityDelta: (10000n * 10n ** 6n).toString(), + slippageLimitLower: getGlobalMinSqrtPrice().toString(), + slippageLimitUpper: getGlobalMaxSqrtPrice().toString() + }); + + const oraixUsdt = await handler.getPool(oraixUsdtPK); + const usdcOraix = await handler.getPool(usdcOraixPK); + const usdcUsdt = await handler.getPool(usdcUsdtPK); + + // console.log({ oraixUsdt, usdcOraix, usdcUsdt }); + + const positions = await handler.allPositions(); + const liquidityOraixUsdt = await handler.getPairLiquidityValues(oraixUsdt, positions); + const liquidityUsdcOraix = await handler.getPairLiquidityValues(usdcOraix, positions); + const liquidityUsdcUsdt = await handler.getPairLiquidityValues(usdcUsdt, positions); + }); }); }); diff --git a/yarn.lock b/yarn.lock index 8612f5c9..6b2be0b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3657,6 +3657,11 @@ resolved "https://registry.yarnpkg.com/@oraichain/immutable/-/immutable-4.3.9.tgz#ff8d5a7b39b5b01f3f72a902cffbfea32ccb20c3" integrity sha512-INpHnhL970OCkR7I71Kssb2aLl2l4Y/x8W6FlyRO0KmC8GHjxc/hlNB1t44BiI7lkOYmcWMRQoC8dwParsp1RQ== +"@oraichain/oraidex-contracts-sdk@^1.0.24", "@oraichain/oraidex-contracts-sdk@^1.0.49": + version "1.0.51" + resolved "https://registry.yarnpkg.com/@oraichain/oraidex-contracts-sdk/-/oraidex-contracts-sdk-1.0.51.tgz#395261248398a4cac15625923c4f97888115ca28" + integrity sha512-a/ajIST9JuJAB9yU/OwFhd3RjBlDPvAunMi19XSljiP4y9FLvo8Lht3KEnwELuQp1kVW1Iw1/B/S5dxmlcM4SQ== + "@oraichain/oraidex-contracts-sdk@latest": version "1.0.44" resolved "https://registry.yarnpkg.com/@oraichain/oraidex-contracts-sdk/-/oraidex-contracts-sdk-1.0.44.tgz#9ff41ec388dd92ba112c2eef545d11fd6e18c684"