From c053c8c340a9beddf86e988519232a6a1e7f5431 Mon Sep 17 00:00:00 2001 From: Shunji Zhan Date: Wed, 27 Mar 2024 14:32:01 +0800 Subject: [PATCH] polish --- README.md | 4 +-- scripts/e2e-prod.ts | 3 ++- src/__tests__/route.test.ts | 14 +++++------ src/api/homa.ts | 44 +++++++++++++++++++------------- src/utils/error.ts | 2 +- src/utils/index.ts | 1 + src/utils/relay.ts | 2 +- src/utils/token.ts | 29 +++++++++++++++++++++ src/utils/utils.ts | 50 +++++++++++++++++-------------------- 9 files changed, 92 insertions(+), 57 deletions(-) create mode 100644 src/utils/token.ts diff --git a/README.md b/README.md index 2351469..47a6991 100644 --- a/README.md +++ b/README.md @@ -426,10 +426,10 @@ data: { => route id { - data: 'homa-0' + data: 'homa-1711514333845' } -GET /routeStatus?routeId=homa-0 +GET /routeStatus?routeId=homa-1711514333845 => route status { data: { status: 0 } } // waiting for token { data: { status: 1 } } // token arrived, routing diff --git a/scripts/e2e-prod.ts b/scripts/e2e-prod.ts index b1816bf..6848ead 100644 --- a/scripts/e2e-prod.ts +++ b/scripts/e2e-prod.ts @@ -74,7 +74,8 @@ const routeWormhole = async (chainId: ROUTER_CHAIN_ID) => { const routerAddr = res.data.data.routerAddr; console.log({ routerAddr }); // 0x0FF0e74513fE82A0c4830309811f1aC1e5d06055 / 0xAAbc44730778B9Dc76fA0B1E65eBeF28D8B7B086 - const provider = new AcalaJsonRpcProvider(chainId === CHAIN_ID_KARURA ? ETH_RPC.KARURA : ETH_RPC.ACALA); + const ethRpcUrl = chainId === CHAIN_ID_KARURA ? ETH_RPC.KARURA : ETH_RPC.ACALA; + const provider = new AcalaJsonRpcProvider(ethRpcUrl); const wallet = new Wallet(key, provider); const token = chainId === CHAIN_ID_KARURA diff --git a/src/__tests__/route.test.ts b/src/__tests__/route.test.ts index dda00dd..96e348a 100644 --- a/src/__tests__/route.test.ts +++ b/src/__tests__/route.test.ts @@ -12,7 +12,7 @@ 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 { formatEther, parseEther, parseUnits } from 'ethers/lib/utils'; +import { parseEther, parseUnits } from 'ethers/lib/utils'; import assert from 'assert'; import { @@ -469,12 +469,12 @@ describe.skip('/routeHoma', () => { // user should receive LDOT const routingFee = await fee.getFee(DOT); - const exchangeRate = parseEther((1 / Number(formatEther(await homa.getExchangeRate()))).toString()); // 10{18} DOT => ? LDOT - const expectedLdot = parsedStakeAmount.sub(routingFee).mul(exchangeRate).div(ONE_ACA); + 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)).to.eq(parsedStakeAmount); // TODO: why this has a super slight off? + 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()); @@ -543,12 +543,12 @@ describe.skip('/routeHoma', () => { // user should receive LDOT const routingFee = await fee.getFee(DOT); - const exchangeRate = parseEther((1 / Number(formatEther(await homa.getExchangeRate()))).toString()); // 10{18} DOT => ? LDOT - const expectedLdot = parsedStakeAmount.sub(routingFee).mul(exchangeRate).div(ONE_ACA); + 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)).to.eq(parsedStakeAmount); // TODO: why this has a super slight off? + 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()); diff --git a/src/api/homa.ts b/src/api/homa.ts index 09f3e39..1895375 100644 --- a/src/api/homa.ts +++ b/src/api/homa.ts @@ -12,6 +12,7 @@ import { getChainConfig, getMainnetChainId, routeStatusParams, + runWithRetry, toAddr32, } from '../utils'; @@ -66,10 +67,10 @@ export enum RouteStatus { interface RouteInfo { status: RouteStatus; - [key: string]: any; + txHash?: string; + err?: any; } -let routeReqId = 0; const routeTracker: Record = {}; export const routeHomaAuto = async (params: RouteParamsHoma) => { const { chain, destAddr } = params; @@ -79,15 +80,15 @@ export const routeHomaAuto = async (params: RouteParamsHoma) => { throw new RouteError(msg, params); } - const reqId = `homa-${routeReqId++}`; - routeTracker[reqId] = { status: RouteStatus.Waiting }; + const reqId = `homa-${Date.now()}`; + const tracker = routeTracker[reqId] = { status: RouteStatus.Waiting } as RouteInfo; const { homaFactory, feeAddr, routeToken, wallet } = await prepareRouteHoma(chain); const dotOrKsm = ERC20__factory.connect(routeToken, wallet); const waitForToken = new Promise((resolve, reject) => { const id = setInterval(async () => { - const bal = await dotOrKsm.balanceOf(routerAddr!); // TODO: probably should add retry here to make sure this won't throw + const bal = await runWithRetry(() => dotOrKsm.balanceOf(routerAddr!)); if (bal.gt(0)) { clearInterval(id); resolve(); @@ -102,30 +103,37 @@ export const routeHomaAuto = async (params: RouteParamsHoma) => { }); waitForToken.then(async () => { - routeTracker[reqId] = { status: RouteStatus.Routing }; + tracker.status = RouteStatus.Routing; let tx: ContractTransaction; try { - tx = await homaFactory.deployHomaRouterAndRoute(feeAddr, toAddr32(destAddr), routeToken); + tx = await runWithRetry( + () => homaFactory.deployHomaRouterAndRoute(feeAddr, toAddr32(destAddr), routeToken), + { retry: 3, interval: 20 } + ); } catch (err) { - routeTracker[reqId] = { status: RouteStatus.Failed, err: err.message }; + tracker.status = RouteStatus.Failed; + tracker.err = err.message; return; } - const txhash = tx.hash; - routeTracker[reqId] = { status: RouteStatus.Confirming, txhash }; - const receipt = await tx.wait(); + tracker.txHash = tx.hash; + tracker.status = RouteStatus.Confirming; - routeTracker[reqId] = receipt.status === 0 - ? { status: RouteStatus.Failed, txhash } - : { status: RouteStatus.Complete, txhash }; + const receipt = await runWithRetry(() => tx.wait(), { retry: 3, interval: 10 }); + tracker.status = receipt.status === 0 + ? RouteStatus.Failed + : RouteStatus.Complete; }).catch(err => { - routeTracker[reqId] = err === 'timeout' - ? { status: RouteStatus.Timeout } - : { status: RouteStatus.Failed, err: err.message }; + if (err === 'timeout') { + tracker.status = RouteStatus.Timeout; + } else { + tracker.status = RouteStatus.Failed; + tracker.err = err; + } }); - // clear after 7 days to avoid memory blow + // clear record after 7 days to avoid memory blow setTimeout(() => delete routeTracker[reqId], 7 * 24 * 60 * 60 * 1000); return reqId; diff --git a/src/utils/error.ts b/src/utils/error.ts index ebf99d9..6b017a2 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -27,4 +27,4 @@ export class RouteError extends RelayerError { super(message, params); this.name = 'RouteError'; } -}; \ No newline at end of file +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index 34bf4ba..1f22026 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -7,3 +7,4 @@ export * from './validate'; export * from './wormhole'; export * from './address'; export * from './error'; +export * from './token'; diff --git a/src/utils/relay.ts b/src/utils/relay.ts index 05d39ca..f2060d0 100644 --- a/src/utils/relay.ts +++ b/src/utils/relay.ts @@ -11,9 +11,9 @@ import { import { ChainConfig } from './configureEnv'; import { RELAYER_SUPPORTED_ADDRESSES_AND_THRESHOLDS } from '../consts'; import { RelayAndRouteParams } from './validate'; +import { RelayError } from './error'; import { VaaInfo, parseVaaPayload } from './wormhole'; import { logger } from './logger'; -import { RelayError } from './error'; interface ShouldRelayResult { shouldRelay: boolean; diff --git a/src/utils/token.ts b/src/utils/token.ts new file mode 100644 index 0000000..3035e12 --- /dev/null +++ b/src/utils/token.ts @@ -0,0 +1,29 @@ +import { BigNumber, Signer } from 'ethers'; +import { ERC20__factory } from '@acala-network/asset-router/dist/typechain-types'; +import { Provider } from '@ethersproject/abstract-provider'; +import { formatUnits, parseUnits } from 'ethers/lib/utils'; + +export const parseAmount = async ( + tokenAddr: string, + amount: string, + signerOrProvider: Signer | Provider, +): Promise => { + const token = ERC20__factory.connect(tokenAddr, signerOrProvider); + const decimals = await token.decimals(); + + return parseUnits(amount, decimals); +}; + +export const getTokenBalance = async ( + tokenAddr: string, + signer: Signer, +): Promise => { + const token = ERC20__factory.connect(tokenAddr, signer); + + const [bal, decimals] = await Promise.all([ + token.balanceOf(await signer.getAddress()), + token.decimals(), + ]); + + return formatUnits(bal, decimals); +}; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 8931ab3..ae2ff95 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,5 +1,5 @@ import { ApiPromise, Keyring, WsProvider } from '@polkadot/api'; -import { BigNumber, Contract, PopulatedTransaction, Signer, Wallet } from 'ethers'; +import { BigNumber, PopulatedTransaction, Wallet } from 'ethers'; import { CHAIN_ID_ACALA, CHAIN_ID_AVAX, CHAIN_ID_KARURA, CONTRACTS, hexToUint8Array } from '@certusone/wormhole-sdk'; import { DispatchError } from '@polkadot/types/interfaces'; import { ISubmittableResult } from '@polkadot/types/types'; @@ -7,12 +7,13 @@ import { JsonRpcProvider } from '@ethersproject/providers'; import { SubmittableExtrinsic } from '@polkadot/api/types'; import { SubstrateSigner } from '@acala-network/bodhi'; import { cryptoWaitReady } from '@polkadot/util-crypto'; -import { decodeEthGas } from '@acala-network/eth-providers'; +import { decodeEthGas, sleep } from '@acala-network/eth-providers'; import { options } from '@acala-network/api'; -import { formatUnits, parseUnits } from 'ethers/lib/utils'; -import { bridgeToken, getSignedVAAFromSequence } from './wormhole'; import { RelayerError } from './error'; +import { bridgeToken, getSignedVAAFromSequence } from './wormhole'; +import { parseAmount } from './token'; +import { logger } from './logger'; export type ROUTER_CHAIN_ID = typeof CHAIN_ID_KARURA | typeof CHAIN_ID_ACALA; @@ -31,17 +32,6 @@ export const getApi = async (privateKey: string, nodeUrl: string) => { return { substrateAddr, api }; }; -export const parseAmount = async ( - tokenAddr: string, - amount: string, - provider: any, -): Promise => { - const erc20 = new Contract(tokenAddr, ['function decimals() view returns (uint8)'], provider); - const decimals = await erc20.decimals(); - - return parseUnits(amount, decimals); -}; - export const transferFromAvax = async ( amount: string, sourceAsset: string, @@ -188,15 +178,21 @@ export const getEthExtrinsic = async ( ); }; -export const getTokenBalance = async (tokenAddr: string, signer: Signer): Promise => { - const erc20 = new Contract(tokenAddr, [ - 'function decimals() view returns (uint8)', - 'function balanceOf(address _owner) public view returns (uint256 balance)', - ], signer); - const [bal, decimals] = await Promise.all([ - erc20.balanceOf(await signer.getAddress()), - erc20.decimals(), - ]); - - return formatUnits(bal, decimals); -}; \ No newline at end of file +// TODO: ideally this only retries on certain errors, such as network issues +export const runWithRetry = async ( + fn: () => Promise, + { retry = 10, interval = 5 } = {}, +): Promise => { + let error: any; + for (let i = 0; i < retry; i++) { + try { + return await fn(); + } catch (err) { + error = err; + logger.info(`retrying ${fn.name} in ${interval}s [${i + 1}/${retry}]`); + await sleep(interval); + } + } + + throw error; +};