diff --git a/.github/workflows/publish_package.yml b/.github/workflows/publish_package.yml index 5400a4fe..ca279fe5 100644 --- a/.github/workflows/publish_package.yml +++ b/.github/workflows/publish_package.yml @@ -45,7 +45,7 @@ jobs: if: steps.yarn-cache.outputs.cache-hit != 'true' run: yarn - name: Build - run: yarn build + run: yarn build && yarn build-tsc packages/oraidex-sync && yarn --cwd packages/oraidex-server/ build - name: Authenticate with private NPM package run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc - name: Publish Oraidex Server diff --git a/.github/workflows/publish_staging_package.yml b/.github/workflows/publish_staging_package.yml index 14b3272d..c5758e05 100644 --- a/.github/workflows/publish_staging_package.yml +++ b/.github/workflows/publish_staging_package.yml @@ -48,7 +48,7 @@ jobs: if: steps.yarn-cache.outputs.cache-hit != 'true' run: yarn - name: Build - run: yarn build + run: yarn build-tsc packages/contracts-sdk && yarn build-tsc packages/oraidex-sync && yarn --cwd packages/oraidex-server-staging/ build - name: Authenticate with private NPM package run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc - name: Publish diff --git a/package.json b/package.json index 349f2093..bfb176d2 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,10 @@ "docs": "typedoc --entryPointStrategy expand --name 'Oraidex SDK' --readme none --tsconfig packages/contracts-sdk/tsconfig.json packages/contracts-sdk/src", "clean": "lerna clean --yes && lerna exec -- rimraf build/ dist/ cache/", "build": "lerna run build --concurrency 1", + "build-tsc": "tsc -p", "postbuild": "mkdir -p packages/oraidex-common/build/tronweb && cp -r packages/oraidex-common/src/tronweb/* packages/oraidex-common/build/tronweb", "deploy": "yarn publish --access public", - "start:server": "yarn build packages/oraidex-sync/ && npx ts-node-dev packages/oraidex-server/src/index.ts", + "start:server": "yarn build-tsc packages/oraidex-sync/ && npx ts-node-dev packages/oraidex-server/src/index.ts", "start:sync": "npx ts-node packages/oraidex-sync/src/sync-cmd.ts", "prepare": "husky install" }, diff --git a/packages/oraidex-server/package.json b/packages/oraidex-server/package.json index 6012970e..11c81a8e 100644 --- a/packages/oraidex-server/package.json +++ b/packages/oraidex-server/package.json @@ -1,6 +1,6 @@ { "name": "@oraichain/oraidex-server", - "version": "1.0.18", + "version": "1.0.19", "main": "dist/index.js", "bin": "dist/index.js", "license": "MIT", diff --git a/packages/oraidex-server/package.staging.json b/packages/oraidex-server/package.staging.json index 6101526c..a195fecb 100644 --- a/packages/oraidex-server/package.staging.json +++ b/packages/oraidex-server/package.staging.json @@ -1,6 +1,6 @@ { "name": "@oraichain/oraidex-server-staging", - "version": "1.0.17", + "version": "1.0.20", "main": "dist/index.js", "bin": "dist/index.js", "license": "MIT", diff --git a/packages/oraidex-server/src/index.ts b/packages/oraidex-server/src/index.ts index 12bbbc63..2bce824c 100644 --- a/packages/oraidex-server/src/index.ts +++ b/packages/oraidex-server/src/index.ts @@ -469,6 +469,7 @@ app oraidexSync.sync(); console.log(`[server]: oraiDEX info server is running at http://${hostname}:${port}`); }) - .on("error", () => { + .on("error", (err) => { + console.log("error when start oraiDEX server", err); process.exit(1); }); diff --git a/packages/oraidex-sync/src/helper.ts b/packages/oraidex-sync/src/helper.ts index f620e29c..1d5a7d47 100644 --- a/packages/oraidex-sync/src/helper.ts +++ b/packages/oraidex-sync/src/helper.ts @@ -311,12 +311,8 @@ export function buildOhlcv(ops: SwapOperationData[]): Ohlcv[] { } export const calculateBasePriceFromSwapOp = (op: SwapOperationData): number => { - if (!op || !op.offerAmount || !op.returnAmount) { - return 0; - } - const offerAmount = op.offerAmount; - const askAmount = op.returnAmount; - return op.direction === "Buy" ? Number(offerAmount) / Number(askAmount) : Number(askAmount) / Number(offerAmount); + if (!op || !op.quotePoolAmount || !op.basePoolAmount) return 0; + return Number(op.quotePoolAmount) / Number(op.basePoolAmount); }; export function getSwapDirection(offerDenom: string, askDenom: string): SwapDirection { diff --git a/packages/oraidex-sync/src/tx-parsing.ts b/packages/oraidex-sync/src/tx-parsing.ts index ca8dc1b0..bd504fe0 100644 --- a/packages/oraidex-sync/src/tx-parsing.ts +++ b/packages/oraidex-sync/src/tx-parsing.ts @@ -22,7 +22,13 @@ import { } from "./helper"; import { pairWithStakingAsset, pairs } from "./pairs"; import { parseAssetInfoOnlyDenom, parseCw20DenomToAssetInfo } from "./parse"; -import { accumulatePoolAmount, calculateLiquidityFee, getPriceAssetByUsdt, isPoolHasFee } from "./pool-helper"; +import { + accumulatePoolAmount, + calculateLiquidityFee, + getPoolInfos, + getPriceAssetByUsdt, + isPoolHasFee +} from "./pool-helper"; import { fetchAllRewardInfo } from "./query"; import { AccountTx, @@ -34,12 +40,15 @@ import { MsgType, OraiswapPairCw20HookMsg, OraiswapRouterCw20HookMsg, + PoolInfo, ProvideLiquidityOperationData, StakingOperationData, SwapOperationData, TxAnlysisResult, WithdrawLiquidityOperationData } from "./types"; +import { AssetInfo } from "@oraichain/oraidex-contracts-sdk"; +import { PoolResponse } from "@oraichain/oraidex-contracts-sdk/build/OraiswapPair.types"; function parseWasmEvents(events: readonly Event[]): (readonly Attribute[])[] { return events.filter((event) => event.type === "wasm").map((event) => event.attributes); @@ -151,6 +160,84 @@ async function calculateLpPrice(stakingAssetDenom: string): Promise { } } +export const getBaseQuoteAmountFromSwapOps = (swapOp: SwapOperationData) => { + // Sell: offer is ORAI, return is USDT + // Buy: offer is USDT, return is ORAI + let baseAmount = swapOp.direction === "Sell" ? swapOp.offerAmount : swapOp.returnAmount; + let quoteAmount = -(swapOp.direction === "Sell" ? swapOp.returnAmount : swapOp.offerAmount); + if (swapOp.direction === "Buy") { + baseAmount = -baseAmount; + quoteAmount = -quoteAmount; + } + return [BigInt(baseAmount), BigInt(quoteAmount)]; +}; + +export const getPoolFromSwapDenom = (swapOp: SwapOperationData, poolInfos: PoolResponse[]) => { + const baseDenom = swapOp.direction === "Sell" ? swapOp.offerDenom : swapOp.askDenom; + const quoteDenom = swapOp.direction === "Sell" ? swapOp.askDenom : swapOp.offerDenom; + const pool = poolInfos.find( + (info) => + info.assets.some((assetInfo) => parseAssetInfoOnlyDenom(assetInfo.info) === baseDenom) && + info.assets.some((assetInfo) => parseAssetInfoOnlyDenom(assetInfo.info) === quoteDenom) + ); + return pool; +}; + +export const calculateSwapOpsWithPoolAmount = async (swapOps: SwapOperationData[]): Promise => { + try { + if (swapOps.length === 0) return []; + const duckDb = DuckDb.instances; + const pairInfos = await duckDb.queryPairInfos(); + + const minTxHeight = swapOps[0].txheight; + const poolInfos = await getPoolInfos( + pairInfos.map((pair) => pair.pairAddr), + minTxHeight - 1 // assume data is sorted by height and timestamp + ); + let accumulatePoolAmount: { + [key: string]: PoolInfo; + } = {}; + + let updatedSwapOps = JSON.parse(JSON.stringify(swapOps)) as SwapOperationData[]; + for (const swapOp of updatedSwapOps) { + const pool = getPoolFromSwapDenom(swapOp, poolInfos); + + // get pair addr to combine by address + let assetInfos = pool.assets.map((asset) => asset.info) as [AssetInfo, AssetInfo]; + if (isAssetInfoPairReverse(assetInfos)) assetInfos.reverse(); + const pairInfo = await duckDb.getPoolByAssetInfos(assetInfos); + if (!pairInfo) throw new Error("cannot find pair info when collectAccumulateLpAndSwapData"); + const { pairAddr } = pairInfo; + + const [baseAmount, quoteAmount] = getBaseQuoteAmountFromSwapOps(swapOp); + // accumulate pool amount by pair addr + if (!accumulatePoolAmount[pairAddr]) { + let initialFirstTokenAmount = BigInt( + pool.assets.find((asset) => parseAssetInfoOnlyDenom(asset.info) === parseAssetInfoOnlyDenom(assetInfos[0])) + .amount + ); + let initialSecondTokenAmount = BigInt( + pool.assets.find((asset) => parseAssetInfoOnlyDenom(asset.info) === parseAssetInfoOnlyDenom(assetInfos[1])) + .amount + ); + accumulatePoolAmount[pairAddr] = { + offerPoolAmount: initialFirstTokenAmount + baseAmount, + askPoolAmount: initialSecondTokenAmount + quoteAmount + }; + } else { + accumulatePoolAmount[pairAddr].offerPoolAmount += baseAmount; + accumulatePoolAmount[pairAddr].askPoolAmount += quoteAmount; + } + // update pool amount for swap ops + swapOp.basePoolAmount = accumulatePoolAmount[pairAddr].offerPoolAmount; + swapOp.quotePoolAmount = accumulatePoolAmount[pairAddr].askPoolAmount; + } + return updatedSwapOps; + } catch (error) { + console.log("error in calculateSwapOpsWithPoolAmount: ", error.message); + } +}; + async function extractStakingOperations( txData: BasicTxData, wasmAttributes: (readonly Attribute[])[] @@ -550,9 +637,10 @@ async function parseTxs(txs: Tx[]): Promise { // accumulate liquidity pool amount via provide/withdraw liquidity and swap ops const poolAmountHistories = await accumulatePoolAmount(lpOpsData, [...swapOpsData]); + const swapOpsWithPoolAmount = await calculateSwapOpsWithPoolAmount(swapOpsData); return { swapOpsData: groupByTime(swapOpsData) as SwapOperationData[], - ohlcv: buildOhlcv(swapOpsData), + ohlcv: buildOhlcv(swapOpsWithPoolAmount), accountTxs, provideLiquidityOpsData, withdrawLiquidityOpsData, diff --git a/packages/oraidex-sync/src/types.ts b/packages/oraidex-sync/src/types.ts index dde5b1aa..799367cd 100644 --- a/packages/oraidex-sync/src/types.ts +++ b/packages/oraidex-sync/src/types.ts @@ -22,6 +22,8 @@ export type SwapOperationData = { returnAmount: number | bigint; spreadAmount: number; taxAmount: number; + basePoolAmount?: number | bigint; + quotePoolAmount?: number | bigint; } & BasicTxData; export type StakingOperationData = { diff --git a/packages/oraidex-sync/tests/helper.spec.ts b/packages/oraidex-sync/tests/helper.spec.ts index 88772590..de71ec64 100644 --- a/packages/oraidex-sync/tests/helper.spec.ts +++ b/packages/oraidex-sync/tests/helper.spec.ts @@ -582,30 +582,35 @@ describe("test-helper", () => { }); it.each([ - ["Buy" as SwapDirection, 2], - ["Sell" as SwapDirection, 0.5] - ])("test-calculateBasePriceFromSwapOp", (direction: SwapDirection, expectedPrice: number) => { - const swapOp = { - offerAmount: 2, - offerDenom: ORAI, - returnAmount: 1, - askDenom: usdtCw20Address, - direction, - uniqueKey: "1", - timestamp: 1, - txCreator: "a", - txhash: "a", - txheight: 1, - spreadAmount: 1, - taxAmount: 1, - commissionAmount: 1 - } as SwapOperationData; - // first case undefined, return 0 - expect(calculateBasePriceFromSwapOp(undefined as any)).toEqual(0); - // other cases - const price = calculateBasePriceFromSwapOp(swapOp); - expect(price).toEqual(expectedPrice); - }); + ["Buy" as SwapDirection, 100n, 200n, 2], + ["Sell" as SwapDirection, 105n, 214n, 2.038095238095238] + ])( + "test-calculateBasePriceFromSwapOp", + (direction: SwapDirection, basePoolAmount, quotePoolAmount, expectedPrice: number) => { + const swapOp = { + offerAmount: 2, + offerDenom: ORAI, + returnAmount: 1, + askDenom: usdtCw20Address, + direction, + uniqueKey: "1", + timestamp: 1, + txCreator: "a", + txhash: "a", + txheight: 1, + spreadAmount: 1, + taxAmount: 1, + commissionAmount: 1, + basePoolAmount, + quotePoolAmount + } as SwapOperationData; + // first case undefined, return 0 + expect(calculateBasePriceFromSwapOp(undefined as any)).toEqual(0); + // other cases + const price = calculateBasePriceFromSwapOp(swapOp); + expect(price).toEqual(expectedPrice); + } + ); it.each([ [usdtCw20Address, "orai", "Buy" as SwapDirection], diff --git a/packages/oraidex-sync/tests/tx-parsing.spec.ts b/packages/oraidex-sync/tests/tx-parsing.spec.ts index ace5291b..8637f462 100644 --- a/packages/oraidex-sync/tests/tx-parsing.spec.ts +++ b/packages/oraidex-sync/tests/tx-parsing.spec.ts @@ -2,8 +2,11 @@ import * as parse from "../src/tx-parsing"; import { Tx } from "@oraichain/cosmos-rpc-sync"; import { parseTxToMsgExecuteContractMsgs } from "../src/tx-parsing"; import { Tx as CosmosTx } from "cosmjs-types/cosmos/tx/v1beta1/tx"; -import { DuckDb, usdtCw20Address } from "../src"; +import { DuckDb, ORAI, SwapDirection, SwapOperationData, oraiInfo, usdtCw20Address, usdtInfo } from "../src"; import * as helper from "../src/helper"; +import * as poolHelper from "../src/pool-helper"; +import { PoolResponse } from "@oraichain/oraidex-contracts-sdk/build/OraiswapPair.types"; +import { AssetInfo } from "@oraichain/oraidex-contracts-sdk"; describe("test-tx-parsing", () => { it.each<[string, string[]]>([ [ @@ -132,4 +135,146 @@ describe("test-tx-parsing", () => { // assertion expect(LPPrice).toEqual(expectedResult); }); + + it.each([ + ["Sell" as SwapDirection, 2n, -1n], + ["Buy" as SwapDirection, -1n, 2n] + ])("test-getBaseQuoteAmountFromSwapOps", (direction: SwapDirection, expectedBaseAmount, expectedQuoteAmount) => { + // setup + const swapOp: SwapOperationData = { + offerAmount: 2, + returnAmount: 1, + offerDenom: ORAI, + askDenom: usdtCw20Address, + direction, + uniqueKey: "1", + timestamp: 1, + txhash: "a", + txheight: 1, + spreadAmount: 1, + taxAmount: 1, + commissionAmount: 1 + }; + + // act + const [baseAmount, quoteAmount] = parse.getBaseQuoteAmountFromSwapOps(swapOp); + + // assertion + expect(Number(baseAmount)).toEqual(Number(expectedBaseAmount)); + expect(Number(quoteAmount)).toEqual(Number(expectedQuoteAmount)); + }); + + it.each([ + ["Sell" as SwapDirection, 2n, -1n], + ["Buy" as SwapDirection, -1n, 2n] + ])("test-getPoolFromSwapDenom", (direction: SwapDirection, expectedBaseAmount, expectedQuoteAmount) => { + // setup + const swapOp: SwapOperationData = { + offerAmount: 2, + returnAmount: 1, + offerDenom: ORAI, + askDenom: usdtCw20Address, + direction, + uniqueKey: "1", + timestamp: 1, + txhash: "a", + txheight: 1, + spreadAmount: 1, + taxAmount: 1, + commissionAmount: 1 + }; + + const poolInfos: PoolResponse[] = [ + { + assets: [ + { + amount: "1", + info: oraiInfo + }, + { + amount: "1", + info: usdtInfo + } + ], + total_share: "1" + } + ]; + + // act + const pool = parse.getPoolFromSwapDenom(swapOp, poolInfos); + + // assertion + expect(pool).toBeDefined(); + expect(pool?.total_share).toBe("1"); + }); + + it("test-calculateSwapOpsWithPoolAmount-with-empty-array-swap-ops-should-return-empty-array", async () => { + const result = await parse.calculateSwapOpsWithPoolAmount([]); + expect(result.length).toEqual(0); + }); + + it("test-calculateSwapOpsWithPoolAmount", async () => { + // setup + const swapOps: SwapOperationData[] = [ + { + offerAmount: 2, + returnAmount: 1, + offerDenom: usdtCw20Address, + askDenom: ORAI, + direction: "Buy", + uniqueKey: "1", + timestamp: 1, + txhash: "a", + txheight: 1, + spreadAmount: 1, + taxAmount: 1, + commissionAmount: 1 + } + ]; + + // mock queryPairInfos + const pairAddr = "orai1c5s03c3l336dgesne7dylnmhszw8554tsyy9yt"; + const pairInfos = [ + { + firstAssetInfo: JSON.stringify({ native_token: { denom: ORAI } } as AssetInfo), + secondAssetInfo: JSON.stringify({ token: { contract_addr: usdtCw20Address } } as AssetInfo), + commissionRate: "1", + pairAddr, + liquidityAddr: "1", + oracleAddr: "1", + symbols: "1", + fromIconUrl: "1", + toIconUrl: "1" + } + ]; + const duckDb = await DuckDb.create(":memory:"); + jest.spyOn(duckDb, "queryPairInfos").mockResolvedValue(pairInfos); + jest.spyOn(duckDb, "getPoolByAssetInfos").mockResolvedValue(pairInfos[0]); + + // mock poolInfos + const poolInfos: PoolResponse[] = [ + { + assets: [ + { + amount: "100", // base pool amount + info: oraiInfo + }, + { + amount: "200", // quote pool amount + info: usdtInfo + } + ], + total_share: "1" + } + ]; + jest.spyOn(poolHelper, "getPoolInfos").mockResolvedValue(poolInfos); + + // act + const updatedSwapOps = await parse.calculateSwapOpsWithPoolAmount(swapOps); + + // assertion + console.dir({ updatedSwapOps }, { depth: null }); + expect(updatedSwapOps[0].basePoolAmount).toEqual(99n); // 100n - 1n + expect(updatedSwapOps[0].quotePoolAmount).toEqual(202n); // 200n + 2n + }); });