Skip to content

Commit

Permalink
supports auto route homa (#61)
Browse files Browse the repository at this point in the history
* refactor

* added routeHomaAuto endpoint

* added tests for homa auto route

* support ksm

* added doc

* polish

* use date now as initial req id
  • Loading branch information
shunjizhan committed Mar 27, 2024
1 parent 2700b8a commit a54f73d
Show file tree
Hide file tree
Showing 23 changed files with 646 additions and 322 deletions.
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,43 @@ data: {
}
/* ---------- when error ---------- */
// similar to /routeXcm
```

### `/routeHomaAuto`
wait for the token to arrive at the router, then route the quest automatically. Returns the request id, which can be used to track route progress
```
POST /routeHomaAuto
data: {
destAddr: string; // recepient evm or acala native address
chain: string; // 'acala' or 'karura'
}
```

example
```
POST /routeHomaAuto
data: {
destAddr: 0x0085560b24769dAC4ed057F1B2ae40746AA9aAb6
chain: 'acala'
}
=> route id
{
data: 'homa-1711514333845'
}
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
```

/* ---------- when error ---------- */
// similar to /routeXcm
```
Expand Down
11 changes: 6 additions & 5 deletions scripts/e2e-prod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import dotenv from 'dotenv';
import path from 'path';

import { BSC_TOKEN, ETH_RPC , RELAYER_URL } from '../src/consts';
import { ROUTER_CHAIN_ID } from '../src/utils';
import { getErc20Balance, transferErc20, transferFromBSC } from './scriptUtils';
import { ROUTER_CHAIN_ID, getTokenBalance } from '../src/utils';
import { transferErc20, transferFromBSC } from './scriptUtils';

dotenv.config({ path: path.join(__dirname, '.env') });
const key = process.env.KEY;
Expand Down Expand Up @@ -37,7 +37,7 @@ const routeXcm = async (chainId: ROUTER_CHAIN_ID) => {
const wallet = new Wallet(key, provider);

const token = chainId === CHAIN_ID_KARURA ? BSC_TOKEN.USDC : BSC_TOKEN.DAI;
const bal = await getErc20Balance(token, wallet);
const bal = await getTokenBalance(token, wallet);
if (Number(bal) === 0) {
throw new Error('no token balance to transfer!');
}
Expand Down Expand Up @@ -74,14 +74,15 @@ 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
? ROUTER_TOKEN_INFO.usdc.karuraAddr
: ROUTER_TOKEN_INFO.dai.acalaAddr;

const bal = await getErc20Balance(token, wallet);
const bal = await getTokenBalance(token, wallet);
if (Number(bal) === 0) {
throw new Error('no token balance to transfer!');
}
Expand Down
13 changes: 0 additions & 13 deletions scripts/scriptUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,6 @@ import {
parseAmount,
} from '../src/utils';

export const getErc20Balance = async (tokenAddr: string, signer: Signer): Promise<string> => {
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);
};

export const transferErc20 = async (
tokenAddr: string,
amount: string,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
endpoint:
- wss://crosschain-dev.polkawallet.io:9915
- wss://acala-rpc.aca-api.network
mock-signature-host: true
# block: ${env.ACALA_BLOCK_NUMBER}
db: ./db.sqlite
Expand Down
102 changes: 98 additions & 4 deletions src/__tests__/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ 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 {
BASILISK_TESTNET_NODE_URL,
Expand All @@ -22,6 +23,7 @@ import {
TEST_KEY,
} from './testConsts';
import { ETH_RPC, FUJI_TOKEN, GOERLI_USDC, PARA_ID } from '../consts';
import { RouteStatus } from '../api';
import {
encodeXcmDest,
expectError,
Expand All @@ -31,6 +33,8 @@ import {
relayAndRoute,
relayAndRouteBatch,
routeHoma,
routeHomaAuto,
routeStatus,
routeWormhole,
routeXcm,
shouldRouteHoma,
Expand Down Expand Up @@ -421,6 +425,9 @@ describe.skip('/routeHoma', () => {
};

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',
Expand Down Expand Up @@ -462,12 +469,86 @@ 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());
};

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<void>((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());
Expand All @@ -484,5 +565,18 @@ describe.skip('/routeHoma', () => {

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);
});
});


30 changes: 15 additions & 15 deletions src/__tests__/shouldRoute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,13 +321,13 @@ describe.concurrent.skip('/shouldRouteHoma', () => {
});

expect(res).toMatchInlineSnapshot(`
{
"data": {
"routerAddr": "0xa013818BBddc5d2d55ab9cCD50759b3B1953d6cd",
"shouldRoute": true,
},
}
`);
{
"data": {
"routerAddr": "0x8A4f03B2D615172f0714AaC2E8C399a6f0d9e448",
"shouldRoute": true,
},
}
`);

// should be case insensitive
res = await shouldRouteHoma({
Expand All @@ -336,13 +336,13 @@ describe.concurrent.skip('/shouldRouteHoma', () => {
});

expect(res).toMatchInlineSnapshot(`
{
"data": {
"routerAddr": "0xa013818BBddc5d2d55ab9cCD50759b3B1953d6cd",
"shouldRoute": true,
},
}
`);
{
"data": {
"routerAddr": "0x8A4f03B2D615172f0714AaC2E8C399a6f0d9e448",
"shouldRoute": true,
},
}
`);
}
});

Expand All @@ -357,7 +357,7 @@ describe.concurrent.skip('/shouldRouteHoma', () => {
expect(res).toMatchInlineSnapshot(`
{
"data": {
"routerAddr": "0xfD6143c380706912a04230f22cF92c402561820e",
"routerAddr": "0x1140EFc2C45e9307701DA521884F75dDDe28f28f",
"shouldRoute": true,
},
}
Expand Down
18 changes: 14 additions & 4 deletions src/__tests__/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import { JsonRpcProvider } from '@ethersproject/providers';
import { Wallet } from 'ethers';
import { expect } from 'vitest';
import { formatEther, parseEther, parseUnits } from 'ethers/lib/utils';
import assert from 'assert';
import axios from 'axios';
import request from 'supertest';

import { ETH_RPC, RELAYER_API, RELAYER_URL } from '../consts';
import { KARURA_USDC_ADDRESS, TEST_KEY } from './testConsts';
import { createApp } from '../app';
import { transferFromAvax } from '../utils';
import { getTokenBalance, transferFromAvax } from '../utils';

export const transferFromFujiToKaruraTestnet = async (
amount: string,
Expand All @@ -22,9 +23,10 @@ export const transferFromFujiToKaruraTestnet = async (
const wallet = new Wallet(TEST_KEY.USER, provider);

const bal = await wallet.getBalance();
if (bal.lt(parseEther('0.03'))) {
throw new Error(`${wallet.address} has insufficient balance on fuji! bal: ${formatEther(bal)}`);
}
assert(bal.gte(parseEther('0.03')), `${wallet.address} has insufficient balance on fuji! bal: ${formatEther(bal)}`);

const tokenBal = await getTokenBalance(sourceAsset, wallet);
assert(Number(tokenBal) > Number(amount), `${wallet.address} has insufficient token balance on fuji! ${tokenBal} < ${amount}`);

return await transferFromAvax(
amount,
Expand Down Expand Up @@ -184,6 +186,14 @@ export const routeHoma = process.env.COVERAGE
? _supertestPost(RELAYER_API.ROUTE_HOMA)
: _axiosPost(RELAYER_URL.ROUTE_HOMA);

export const routeHomaAuto = process.env.COVERAGE
? _supertestPost(RELAYER_API.ROUTE_HOMA_AUTO)
: _axiosPost(RELAYER_URL.ROUTE_HOMA_AUTO);

export const routeStatus = process.env.COVERAGE
? _supertestGet(RELAYER_API.ROUTE_STATUS)
: _axiosGet(RELAYER_URL.ROUTE_STATUS);

export const shouldRouteEuphrates = process.env.COVERAGE
? _supertestGet(RELAYER_API.SHOULD_ROUTER_EUPHRATES)
: _axiosGet(RELAYER_URL.SHOULD_ROUTER_EUPHRATES);
Expand Down
Loading

0 comments on commit a54f73d

Please sign in to comment.