From ccd135457ac3ca64b8fd4e83b635072f44c0c1f2 Mon Sep 17 00:00:00 2001 From: Shunji Zhan Date: Tue, 2 Apr 2024 10:51:09 +0800 Subject: [PATCH] auto route polish (#62) * auto route polish * polish * update doc * update * polish --- README.md | 20 ++- src/__tests__/route.test.ts | 224 ----------------------------- src/__tests__/routeHoma.test.ts | 243 ++++++++++++++++++++++++++++++++ src/api/homa.ts | 65 ++++++++- src/consts.ts | 5 + src/utils/validate.ts | 20 ++- 6 files changed, 336 insertions(+), 241 deletions(-) create mode 100644 src/__tests__/routeHoma.test.ts diff --git a/README.md b/README.md index 22f2c6b..b30e20b 100644 --- a/README.md +++ b/README.md @@ -431,12 +431,20 @@ data: { GET /routeStatus?routeId=homa-1711514333845 => route status -{ data: { status: 0 } } // waiting for token -{ data: { status: 1 } } // token arrived, routing -{ data: { status: 2, txHash: '0x12345 } } // routing tx submitted, waiting for confirmation -{ data: { status: 3, txHash: '0x12345 } } // routing completed -{ data: { status: -1 } } // routing timeout out (usually becuase no token arrive in 3 min) -{ data: { status: -2, error: 'xxx' } } // routing failed +[{ data: { status: 0 } }] // waiting for token +[{ data: { status: 1 } }] // token arrived, routing +[{ data: { status: 2, txHash: '0x12345' } }] // routing tx submitted, waiting for confirmation +[{ data: { status: 3, txHash: '0x12345' } }] // routing completed +[{ data: { status: -1 } }] // routing timeout out (usually becuase no token arrive in 3 min) +[{ data: { status: -2, error: 'xxx' } }] // routing failed + +GET /routeStatus?destAddr=0x0085560b24769dAC4ed057F1B2ae40746AA9aAb6 +=> all route status for this address +[ + { data: { status: 3, txHash: '0x11111' } }, + { data: { status: 2, txHash: '0x22222' } }, + { data: { status: 1 } }, +] /* ---------- when error ---------- */ // similar to /routeXcm diff --git a/src/__tests__/route.test.ts b/src/__tests__/route.test.ts index 96e348a..dcf71c0 100644 --- a/src/__tests__/route.test.ts +++ b/src/__tests__/route.test.ts @@ -1,19 +1,10 @@ -import { ADDRESSES } from '@acala-network/asset-router/dist/consts'; import { AcalaJsonRpcProvider, sleep } from '@acala-network/eth-providers'; import { ApiPromise, WsProvider } from '@polkadot/api'; import { CHAIN_ID_AVAX, CHAIN_ID_KARURA, CONTRACTS, hexToUint8Array, parseSequenceFromLogEth, redeemOnEth } from '@certusone/wormhole-sdk'; import { ContractReceipt, Wallet } from 'ethers'; -import { DOT, LDOT } from '@acala-network/contracts/utils/AcalaTokens'; import { ERC20__factory } from '@certusone/wormhole-sdk/lib/cjs/ethers-contracts'; -import { EVMAccounts__factory, IHoma__factory } from '@acala-network/contracts/typechain'; -import { EVM_ACCOUNTS, HOMA } from '@acala-network/contracts/utils/Predeploy'; -import { FeeRegistry__factory } from '@acala-network/asset-router/dist/typechain-types'; import { JsonRpcProvider } from '@ethersproject/providers'; -import { ONE_ACA, almostEq, toHuman } from '@acala-network/asset-router/dist/utils'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -import { encodeAddress } from '@polkadot/util-crypto'; -import { parseEther, parseUnits } from 'ethers/lib/utils'; -import assert from 'assert'; import { BASILISK_TESTNET_NODE_URL, @@ -23,7 +14,6 @@ import { TEST_KEY, } from './testConsts'; import { ETH_RPC, FUJI_TOKEN, GOERLI_USDC, PARA_ID } from '../consts'; -import { RouteStatus } from '../api'; import { encodeXcmDest, expectError, @@ -32,12 +22,8 @@ import { mockXcmToRouter, relayAndRoute, relayAndRouteBatch, - routeHoma, - routeHomaAuto, - routeStatus, routeWormhole, routeXcm, - shouldRouteHoma, shouldRouteWormhole, shouldRouteXcm, transferFromFujiToKaruraTestnet, @@ -60,10 +46,6 @@ const dest = encodeXcmDest({ const providerKarura = new AcalaJsonRpcProvider(ETH_RPC.KARURA_TESTNET); const relayerKarura = new Wallet(TEST_KEY.RELAYER, providerKarura); -const providerAcalaFork = new AcalaJsonRpcProvider(ETH_RPC.LOCAL); -const relayerAcalaFork = new Wallet(TEST_KEY.RELAYER, providerAcalaFork); -const userAcalaFork = new Wallet(TEST_KEY.USER, providerAcalaFork); - describe('/routeXcm', () => { const api = new ApiPromise({ provider: new WsProvider(BASILISK_TESTNET_NODE_URL) }); @@ -374,209 +356,3 @@ describe('/routeWormhole', () => { // describe.skip('when should not route', () => {}) }); - -describe.skip('/routeHoma', () => { - const DOT_DECIMALS = 10; - const dot = ERC20__factory.connect(DOT, providerAcalaFork); - const ldot = ERC20__factory.connect(LDOT, providerAcalaFork); - const homa = IHoma__factory.connect(HOMA, providerAcalaFork); - const evmAccounts = EVMAccounts__factory.connect(EVM_ACCOUNTS, providerAcalaFork); - const fee = FeeRegistry__factory.connect(ADDRESSES.ACALA.feeAddr, providerAcalaFork); - const stakeAmount = 6; - const parsedStakeAmount = parseUnits(String(stakeAmount), DOT_DECIMALS); - let routerAddr: string; - - const fetchTokenBalances = async () => { - if (!routerAddr) throw new Error('routerAddr not set'); - - const [ - userBalDot, - relayerBalDot, - routerBalDot, - userBalLdot, - relayerBalLdot, - routerBalLdot, - ] = await Promise.all([ - dot.balanceOf(TEST_ADDR_USER), - dot.balanceOf(TEST_ADDR_RELAYER), - dot.balanceOf(routerAddr), - ldot.balanceOf(TEST_ADDR_USER), - ldot.balanceOf(TEST_ADDR_RELAYER), - ldot.balanceOf(routerAddr), - ]); - - console.log({ - userBalDot: toHuman(userBalDot, DOT_DECIMALS), - relayerBalDot: toHuman(relayerBalDot, DOT_DECIMALS), - routerBalDot: toHuman(routerBalDot, DOT_DECIMALS), - userBalLdot: toHuman(userBalLdot, DOT_DECIMALS), - relayerBalLdot: toHuman(relayerBalLdot, DOT_DECIMALS), - routerBalLdot: toHuman(routerBalLdot, DOT_DECIMALS), - }); - - return { - userBalDot, - relayerBalDot, - routerBalDot, - userBalLdot, - relayerBalLdot, - routerBalLdot, - }; - }; - - const testHomaRouter = async (destAddr: string) => { - const relayerBal = await relayerAcalaFork.getBalance(); - assert(relayerBal.gt(parseEther('10')), `relayer doesn't have enough balance to relay! ${relayerAcalaFork.address}`); - - const routeArgs = { - destAddr, - chain: 'acala', - }; - const res = await shouldRouteHoma(routeArgs); - ({ routerAddr } = res.data); - - // make sure user has enough DOT to transfer to router - const bal = await fetchTokenBalances(); - if (bal.userBalDot.lt(parsedStakeAmount)) { - if (bal.relayerBalDot.lt(parsedStakeAmount)) { - throw new Error('both relayer and user do not have enough DOT to transfer to router!'); - } - - console.log('refilling dot for user ...'); - await (await dot.connect(relayerAcalaFork).transfer(TEST_ADDR_USER, parsedStakeAmount)).wait(); - } - - const bal0 = await fetchTokenBalances(); - - console.log('xcming to router ...'); - await mockXcmToRouter(routerAddr, userAcalaFork, DOT, stakeAmount); - - console.log('routing ...'); - const routeRes = await routeHoma({ - ...routeArgs, - token: DOT, - }); - const txHash = routeRes.data; - console.log(`route finished! txHash: ${txHash}`); - - const bal1 = await fetchTokenBalances(); - - // router should be destroyed - const routerCode = await providerKarura.getCode(routerAddr); - expect(routerCode).to.eq('0x'); - expect(bal1.routerBalDot.toNumber()).to.eq(0); - expect(bal1.routerBalLdot.toNumber()).to.eq(0); - - // user should receive LDOT - const routingFee = await fee.getFee(DOT); - const exchangeRate = await homa.getExchangeRate(); // 10{18} LDOT => ? DOT - const expectedLdot = parsedStakeAmount.sub(routingFee).mul(ONE_ACA).div(exchangeRate); - const ldotReceived = bal1.userBalLdot.sub(bal0.userBalLdot); - - expect(almostEq(expectedLdot, ldotReceived)).to.be.true; - expect(bal0.userBalDot.sub(bal1.userBalDot).toBigInt()).to.eq(parsedStakeAmount.toBigInt()); - - // relayer should receive DOT fee - expect(bal1.relayerBalDot.sub(bal0.relayerBalDot).toBigInt()).to.eq(routingFee.toBigInt()); - }; - - const testAutoHomaRouter = async (destAddr: string) => { - const relayerBal = await relayerAcalaFork.getBalance(); - assert(relayerBal.gt(parseEther('10')), `relayer doesn't have enough balance to relay! ${relayerAcalaFork.address}`); - - const routeArgs = { - destAddr, - chain: 'acala', - }; - const res = await shouldRouteHoma(routeArgs); - ({ routerAddr } = res.data); - - // make sure user has enough DOT to transfer to router - const bal = await fetchTokenBalances(); - if (bal.userBalDot.lt(parsedStakeAmount)) { - if (bal.relayerBalDot.lt(parsedStakeAmount)) { - throw new Error('both relayer and user do not have enough DOT to transfer to router!'); - } - - console.log('refilling dot for user ...'); - await (await dot.connect(relayerAcalaFork).transfer(TEST_ADDR_USER, parsedStakeAmount)).wait(); - } - - const bal0 = await fetchTokenBalances(); - - console.log('sending auto routing request ...'); - const routeRes = await routeHomaAuto({ - ...routeArgs, - token: DOT, - }); - const reqId = routeRes.data; - console.log(`auto route submitted! reqId: ${reqId}`); - - const waitForRoute = new Promise((resolve, reject) => { - const pollRouteStatus = setInterval(async () => { - const res = await routeStatus({ id: reqId }); - // console.log(`current status: ${res.data.status}`); - - if (res.data.status === RouteStatus.Complete) { - resolve(); - clearInterval(pollRouteStatus); - } - }, 1000); - - setTimeout(reject, 100 * 1000); - }); - - console.log('xcming to router ...'); - await mockXcmToRouter(routerAddr, userAcalaFork, DOT, stakeAmount); - - console.log('waiting for auto routing ...'); - await waitForRoute; - - console.log('route complete!'); - const bal1 = await fetchTokenBalances(); - - // router should be destroyed - const routerCode = await providerKarura.getCode(routerAddr); - expect(routerCode).to.eq('0x'); - expect(bal1.routerBalDot.toNumber()).to.eq(0); - expect(bal1.routerBalLdot.toNumber()).to.eq(0); - - // user should receive LDOT - const routingFee = await fee.getFee(DOT); - const exchangeRate = await homa.getExchangeRate(); // 10{18} LDOT => ? DOT - const expectedLdot = parsedStakeAmount.sub(routingFee).mul(ONE_ACA).div(exchangeRate); - const ldotReceived = bal1.userBalLdot.sub(bal0.userBalLdot); - - expect(almostEq(expectedLdot, ldotReceived)).to.be.true; - expect(bal0.userBalDot.sub(bal1.userBalDot).toBigInt()).to.eq(parsedStakeAmount.toBigInt()); - - // relayer should receive DOT fee - expect(bal1.relayerBalDot.sub(bal0.relayerBalDot).toBigInt()).to.eq(routingFee.toBigInt()); - }; - - it('route to evm address', async () => { - await testHomaRouter(TEST_ADDR_USER); - }); - - it('route to substrate address', async () => { - const ACALA_SS58_PREFIX = 10; - const userAccountId = await evmAccounts.getAccountId(TEST_ADDR_USER); - const userSubstrateAddr = encodeAddress(userAccountId, ACALA_SS58_PREFIX); - - await testHomaRouter(userSubstrateAddr); - }); - - it('auto route to evm address', async () => { - await testAutoHomaRouter(TEST_ADDR_USER); - }); - - it('auto route to substrate address', async () => { - const ACALA_SS58_PREFIX = 10; - const userAccountId = await evmAccounts.getAccountId(TEST_ADDR_USER); - const userSubstrateAddr = encodeAddress(userAccountId, ACALA_SS58_PREFIX); - - await testAutoHomaRouter(userSubstrateAddr); - }); -}); - - diff --git a/src/__tests__/routeHoma.test.ts b/src/__tests__/routeHoma.test.ts new file mode 100644 index 0000000..fd58c9f --- /dev/null +++ b/src/__tests__/routeHoma.test.ts @@ -0,0 +1,243 @@ +import { ADDRESSES } from '@acala-network/asset-router/dist/consts'; +import { AcalaJsonRpcProvider } from '@acala-network/eth-providers'; +import { DOT, LDOT } from '@acala-network/contracts/utils/AcalaTokens'; +import { ERC20__factory } from '@certusone/wormhole-sdk/lib/cjs/ethers-contracts'; +import { EVMAccounts__factory, IHoma__factory } from '@acala-network/contracts/typechain'; +import { EVM_ACCOUNTS, HOMA } from '@acala-network/contracts/utils/Predeploy'; +import { FeeRegistry__factory } from '@acala-network/asset-router/dist/typechain-types'; +import { ONE_ACA, almostEq, toHuman } from '@acala-network/asset-router/dist/utils'; +import { Wallet } from 'ethers'; +import { describe, expect, it } from 'vitest'; +import { encodeAddress } from '@polkadot/util-crypto'; +import { parseEther, parseUnits } from 'ethers/lib/utils'; +import assert from 'assert'; + +import { ETH_RPC, SECOND } from '../consts'; +import { RouteStatus } from '../api'; +import { + TEST_ADDR_RELAYER, + TEST_ADDR_USER, + TEST_KEY, +} from './testConsts'; +import { + mockXcmToRouter, + routeHoma, + routeHomaAuto, + routeStatus, + shouldRouteHoma, +} from './testUtils'; + +const providerAcalaFork = new AcalaJsonRpcProvider(ETH_RPC.LOCAL); +const relayerAcalaFork = new Wallet(TEST_KEY.RELAYER, providerAcalaFork); +const userAcalaFork = new Wallet(TEST_KEY.USER, providerAcalaFork); + +describe.skip('/routeHoma', () => { + const DOT_DECIMALS = 10; + const dot = ERC20__factory.connect(DOT, providerAcalaFork); + const ldot = ERC20__factory.connect(LDOT, providerAcalaFork); + const homa = IHoma__factory.connect(HOMA, providerAcalaFork); + const evmAccounts = EVMAccounts__factory.connect(EVM_ACCOUNTS, providerAcalaFork); + const fee = FeeRegistry__factory.connect(ADDRESSES.ACALA.feeAddr, providerAcalaFork); + const stakeAmount = 6; + const parsedStakeAmount = parseUnits(String(stakeAmount), DOT_DECIMALS); + let routerAddr: string; + + const fetchTokenBalances = async () => { + if (!routerAddr) throw new Error('routerAddr not set'); + + const [ + userBalDot, + relayerBalDot, + routerBalDot, + userBalLdot, + relayerBalLdot, + routerBalLdot, + ] = await Promise.all([ + dot.balanceOf(TEST_ADDR_USER), + dot.balanceOf(TEST_ADDR_RELAYER), + dot.balanceOf(routerAddr), + ldot.balanceOf(TEST_ADDR_USER), + ldot.balanceOf(TEST_ADDR_RELAYER), + ldot.balanceOf(routerAddr), + ]); + + console.log({ + userBalDot: toHuman(userBalDot, DOT_DECIMALS), + relayerBalDot: toHuman(relayerBalDot, DOT_DECIMALS), + routerBalDot: toHuman(routerBalDot, DOT_DECIMALS), + userBalLdot: toHuman(userBalLdot, DOT_DECIMALS), + relayerBalLdot: toHuman(relayerBalLdot, DOT_DECIMALS), + routerBalLdot: toHuman(routerBalLdot, DOT_DECIMALS), + }); + + return { + userBalDot, + relayerBalDot, + routerBalDot, + userBalLdot, + relayerBalLdot, + routerBalLdot, + }; + }; + + const testHomaRouter = async (destAddr: string) => { + const relayerBal = await relayerAcalaFork.getBalance(); + assert(relayerBal.gt(parseEther('10')), `relayer doesn't have enough balance to relay! ${relayerAcalaFork.address}`); + + const routeArgs = { + destAddr, + chain: 'acala', + }; + const res = await shouldRouteHoma(routeArgs); + ({ routerAddr } = res.data); + + // make sure user has enough DOT to transfer to router + const bal = await fetchTokenBalances(); + if (bal.userBalDot.lt(parsedStakeAmount)) { + if (bal.relayerBalDot.lt(parsedStakeAmount)) { + throw new Error('both relayer and user do not have enough DOT to transfer to router!'); + } + + console.log('refilling dot for user ...'); + await (await dot.connect(relayerAcalaFork).transfer(TEST_ADDR_USER, parsedStakeAmount)).wait(); + } + + const bal0 = await fetchTokenBalances(); + + console.log('xcming to router ...'); + await mockXcmToRouter(routerAddr, userAcalaFork, DOT, stakeAmount); + + console.log('routing ...'); + const routeRes = await routeHoma({ + ...routeArgs, + token: DOT, + }); + const txHash = routeRes.data; + console.log(`route finished! txHash: ${txHash}`); + + const bal1 = await fetchTokenBalances(); + + // router should be destroyed + const routerCode = await providerAcalaFork.getCode(routerAddr); + expect(routerCode).to.eq('0x'); + expect(bal1.routerBalDot.toNumber()).to.eq(0); + expect(bal1.routerBalLdot.toNumber()).to.eq(0); + + // user should receive LDOT + const routingFee = await fee.getFee(DOT); + const exchangeRate = await homa.getExchangeRate(); // 10{18} LDOT => ? DOT + const expectedLdot = parsedStakeAmount.sub(routingFee).mul(ONE_ACA).div(exchangeRate); + const ldotReceived = bal1.userBalLdot.sub(bal0.userBalLdot); + + expect(almostEq(expectedLdot, ldotReceived)).to.be.true; + expect(bal0.userBalDot.sub(bal1.userBalDot).toBigInt()).to.eq(parsedStakeAmount.toBigInt()); + + // relayer should receive DOT fee + expect(bal1.relayerBalDot.sub(bal0.relayerBalDot).toBigInt()).to.eq(routingFee.toBigInt()); + }; + + const testAutoHomaRouter = async (destAddr: string) => { + const relayerBal = await relayerAcalaFork.getBalance(); + assert(relayerBal.gt(parseEther('10')), `relayer doesn't have enough balance to relay! ${relayerAcalaFork.address}`); + + const routeArgs = { + destAddr, + chain: 'acala', + }; + const res = await shouldRouteHoma(routeArgs); + ({ routerAddr } = res.data); + + // make sure user has enough DOT to transfer to router + const bal = await fetchTokenBalances(); + if (bal.userBalDot.lt(parsedStakeAmount)) { + if (bal.relayerBalDot.lt(parsedStakeAmount)) { + throw new Error('both relayer and user do not have enough DOT to transfer to router!'); + } + + console.log('refilling dot for user ...'); + await (await dot.connect(relayerAcalaFork).transfer(TEST_ADDR_USER, parsedStakeAmount)).wait(); + } + + const bal0 = await fetchTokenBalances(); + + console.log('sending auto routing request ...'); + const routeRes = await routeHomaAuto({ + ...routeArgs, + token: DOT, + }); + const reqId = routeRes.data; + console.log(`auto route submitted! reqId: ${reqId}`); + + const waitForRoute = new Promise((resolve, reject) => { + const pollRouteStatus = setInterval(async () => { + const res = await routeStatus({ id: reqId }); + const { status } = res.data[0]; + console.log(`current status: ${status}`); + + if (status === RouteStatus.Complete) { + resolve(); + clearInterval(pollRouteStatus); + } + }, 1 * SECOND); + + setTimeout(reject, 100 * SECOND); + }); + + console.log('xcming to router ...'); + await mockXcmToRouter(routerAddr, userAcalaFork, DOT, stakeAmount); + + console.log('waiting for auto routing ...'); + await waitForRoute; + + // query status by destAddr should also returns same result + const { data } = await routeStatus({ destAddr }); + const reqInfo = data.find(info => info.reqId === reqId); + expect(reqInfo).not.to.be.undefined; + expect(reqInfo.status).to.eq(RouteStatus.Complete); + + console.log('route complete!'); + const bal1 = await fetchTokenBalances(); + + // router should be destroyed + const routerCode = await providerAcalaFork.getCode(routerAddr); + expect(routerCode).to.eq('0x'); + expect(bal1.routerBalDot.toNumber()).to.eq(0); + expect(bal1.routerBalLdot.toNumber()).to.eq(0); + + // user should receive LDOT + const routingFee = await fee.getFee(DOT); + const exchangeRate = await homa.getExchangeRate(); // 10{18} LDOT => ? DOT + const expectedLdot = parsedStakeAmount.sub(routingFee).mul(ONE_ACA).div(exchangeRate); + const ldotReceived = bal1.userBalLdot.sub(bal0.userBalLdot); + + expect(almostEq(expectedLdot, ldotReceived)).to.be.true; + expect(bal0.userBalDot.sub(bal1.userBalDot).toBigInt()).to.eq(parsedStakeAmount.toBigInt()); + + // relayer should receive DOT fee + expect(bal1.relayerBalDot.sub(bal0.relayerBalDot).toBigInt()).to.eq(routingFee.toBigInt()); + }; + + it('route to evm address', async () => { + await testHomaRouter(TEST_ADDR_USER); + }); + + it('route to substrate address', async () => { + const ACALA_SS58_PREFIX = 10; + const userAccountId = await evmAccounts.getAccountId(TEST_ADDR_USER); + const userSubstrateAddr = encodeAddress(userAccountId, ACALA_SS58_PREFIX); + + await testHomaRouter(userSubstrateAddr); + }); + + it('auto route to evm address', async () => { + await testAutoHomaRouter(TEST_ADDR_USER); + }); + + it('auto route to substrate address', async () => { + const ACALA_SS58_PREFIX = 10; + const userAccountId = await evmAccounts.getAccountId(TEST_ADDR_USER); + const userSubstrateAddr = encodeAddress(userAccountId, ACALA_SS58_PREFIX); + + await testAutoHomaRouter(userSubstrateAddr); + }); +}); diff --git a/src/api/homa.ts b/src/api/homa.ts index 6a4c9ed..8d6669c 100644 --- a/src/api/homa.ts +++ b/src/api/homa.ts @@ -3,6 +3,7 @@ import { DOT } from '@acala-network/contracts/utils/AcalaTokens'; import { ERC20__factory, HomaFactory__factory } from '@acala-network/asset-router/dist/typechain-types'; import { KSM } from '@acala-network/contracts/utils/KaruraTokens'; +import { DAY, MINUTE } from '../consts'; import { Mainnet, RouteError, @@ -66,13 +67,43 @@ export enum RouteStatus { }; interface RouteInfo { + reqId: string; status: RouteStatus; + destAddr: string; + routerAddr: string; + timestamp: number; + params: RouteParamsHoma; txHash?: string; err?: any; } +type RouteTracker = Record; + +const cleanUpTracker = ( + tracker: RouteTracker, + maxDays = 7, + maxCount = 1000, +) => { + setTimeout(() => { + const isSizeOk = Object.keys(tracker).length < maxCount; + if (isSizeOk || maxDays <= 1) return; // record should be kept for at least 1 day + + const now = Date.now(); + Object.keys(tracker).forEach(reqId => { + const info = tracker[reqId]; + const age = now - info.timestamp; + if (age > maxDays * DAY) { + delete tracker[reqId]; + } + }); + + // resursively clean up with narrower age range until size < maxCount + cleanUpTracker(tracker, maxDays - 1, maxCount); + }, 0); +}; + let routeReqId = Date.now(); -const routeTracker: Record = {}; +const routeTracker: RouteTracker = {}; export const routeHomaAuto = async (params: RouteParamsHoma) => { const { chain, destAddr } = params; const { routerAddr, shouldRoute, msg } = await shouldRouteHoma(params); @@ -82,7 +113,14 @@ export const routeHomaAuto = async (params: RouteParamsHoma) => { } const reqId = `homa-${routeReqId++}`; - const tracker = routeTracker[reqId] = { status: RouteStatus.Waiting } as RouteInfo; + const tracker: RouteInfo = routeTracker[reqId] = { + reqId, + status: RouteStatus.Waiting, + destAddr, + routerAddr: routerAddr!, + timestamp: Date.now(), + params, + }; const { homaFactory, feeAddr, routeToken, wallet } = await prepareRouteHoma(chain); const dotOrKsm = ERC20__factory.connect(routeToken, wallet); @@ -96,7 +134,7 @@ export const routeHomaAuto = async (params: RouteParamsHoma) => { } }, 3000); - const timeout = 3 * 60 * 1000; // 3 min + const timeout = (params.timeout ?? 5) * MINUTE; setTimeout(() => { clearInterval(id); reject('timeout'); @@ -134,11 +172,24 @@ export const routeHomaAuto = async (params: RouteParamsHoma) => { } }); - // clear record after 7 days to avoid memory blow - setTimeout(() => delete routeTracker[reqId], 7 * 24 * 60 * 60 * 1000); + // clean up trakcer in the background + cleanUpTracker(routeTracker); return reqId; }; -export const getRouteStatus = async ({ id }: routeStatusParams) => routeTracker[id]; -export const getAllRouteStatus = async () => routeTracker; +export const getRouteStatus = async ({ id, destAddr }: routeStatusParams): Promise => { + if (id) { + const routeInfo = routeTracker[id]; + return routeInfo ? [routeInfo] : []; + } else { + return Object.values(routeTracker).filter( + info => info.destAddr.toLowerCase() === destAddr!.toLowerCase() + ); + } +}; + +export const getAllRouteStatus = async () => ({ + reqCount: Object.keys(routeTracker).length, + allStatus: routeTracker, +}); diff --git a/src/consts.ts b/src/consts.ts index c598b2e..1f9f2e7 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -253,4 +253,9 @@ export const TESTNET_MODE_WARNING = ` export const EUPHRATES_ADDR = '0x7Fe92EC600F15cD25253b421bc151c51b0276b7D'; export const EUPHRATES_POOLS = ['0', '1', '2', '3']; +export const SECOND = 1000; +export const MINUTE = 60 * SECOND; +export const HOUR = 60 * MINUTE; +export const DAY = 24 * HOUR; + export const VERSION = '1.7.0-0'; diff --git a/src/utils/validate.ts b/src/utils/validate.ts index 650107c..0d59bc4 100644 --- a/src/utils/validate.ts +++ b/src/utils/validate.ts @@ -1,5 +1,5 @@ import { CHAIN_ID_ACALA, CHAIN_ID_KARURA } from '@certusone/wormhole-sdk'; -import { ObjectSchema, mixed, object, string } from 'yup'; +import { ObjectSchema, mixed, number, object, string } from 'yup'; export enum Mainnet { Acala = 'acala', @@ -30,6 +30,7 @@ export interface RouteParamsXcm extends RouteParamsBase { export interface RouteParamsHoma { chain: Mainnet; destAddr: string; // dest evm or acala native address + timeout?: number; // timeout minutes } export interface RouteParamsEuphrates { @@ -43,7 +44,8 @@ export interface RelayAndRouteParams extends RouteParamsXcm { } export interface routeStatusParams { - id: string; + id?: string; + destAddr?: string; } export const routeXcmSchema: ObjectSchema = object({ @@ -69,6 +71,9 @@ export const routeWormholeSchema: ObjectSchema = object({ export const routeHomaSchema: ObjectSchema = object({ destAddr: string().required(), chain: mixed().oneOf(Object.values(Mainnet)).required(), + timeout: number() + .min(3, 'timeout must be at least 3 minutes') + .max(30, 'timeout cannot exceed 30 minutes'), }); export const routeEuphratesSchema: ObjectSchema = object({ @@ -78,5 +83,12 @@ export const routeEuphratesSchema: ObjectSchema = object({ }); export const routeStatusSchema: ObjectSchema = object({ - id: string().required(), -}); + id: string(), + destAddr: string(), +}).test( + 'id-or-destAddr', + 'either `id` or `destAddr` is required', + value => + (!!value.id && !value.destAddr) || + (!value.id && !!value.destAddr) +);