Skip to content

Commit

Permalink
feat: support OpenOceanV2 on Metis
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeff-CCH committed Nov 13, 2023
1 parent 04f410f commit 4f45a1e
Show file tree
Hide file tree
Showing 12 changed files with 395 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/ninety-seas-fry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@protocolink/logics': patch
---

support OpenOceanV2 on Metis
2 changes: 2 additions & 0 deletions .env.metis
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
CHAIN_ID=1088
HTTP_RPC_URL=https://andromeda.metis.io/?owner=1088
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@
"lint": "eslint --fix src",
"prepublishOnly": "yarn build",
"test": "mocha",
"test:e2e": "yarn run test:e2e:mainnet && yarn run test:e2e:polygon && yarn run test:e2e:arbitrum",
"test:e2e": "yarn run test:e2e:mainnet && yarn run test:e2e:polygon && yarn run test:e2e:metis && yarn run test:e2e:arbitrum",
"test:e2e:mainnet": "env-cmd -f .env.mainnet hardhat test --grep 'mainnet'",
"test:e2e:polygon": "env-cmd -f .env.polygon hardhat test --grep 'polygon'",
"test:e2e:metis": "env-cmd -f .env.metis hardhat test --grep 'metis'",
"test:e2e:arbitrum": "env-cmd -f .env.arbitrum hardhat test --grep 'arbitrum'",
"test:unit": "mocha --recursive src"
},
Expand Down
37 changes: 37 additions & 0 deletions src/logics/openocean-v2/configs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as common from '@protocolink/common';

export interface Config {
chainId: number;
exchangeAddress: string;
gasPrice: string;
}

export const configs: Config[] = [
{
chainId: common.ChainId.metis,
exchangeAddress: '0x6352a56caadC4F1E25CD6c75970Fa768A3304e64',
gasPrice: '10',
},
];

export const [supportedChainIds, configMap] = configs.reduce(
(accumulator, config) => {
accumulator[0].push(config.chainId);
accumulator[1][config.chainId] = config;
return accumulator;
},
[[], {}] as [number[], Record<number, Config>]
);

export function getChainId(chain: string) {
if (chain === 'metis') return common.ChainId.metis;
else return 0;
}

export function getExchangeAddress(chainId: number) {
return configMap[chainId].exchangeAddress;
}

export function getGasPrice(chainId: number) {
return configMap[chainId].gasPrice;
}
2 changes: 2 additions & 0 deletions src/logics/openocean-v2/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './configs';
export * from './logic.swap-token';
67 changes: 67 additions & 0 deletions src/logics/openocean-v2/logic.swap-token.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { LogicTestCaseWithChainId } from 'test/types';
import { SwapTokenLogic, SwapTokenLogicFields, SwapTokenLogicOptions } from './logic.swap-token';
import * as common from '@protocolink/common';
import { constants, utils } from 'ethers';
import * as core from '@protocolink/core';
import { expect } from 'chai';
import { getExchangeAddress } from './configs';
import { metisTokens } from 'src/logics/openocean-v2/tokens';

describe('OpenOceanV2 SwapTokenLogic', function () {
context('Test getTokenList', async function () {
SwapTokenLogic.supportedChainIds.forEach((chainId) => {
it(`network: ${common.toNetworkId(chainId)}`, async function () {
const logic = new SwapTokenLogic(chainId);
const tokenList = await logic.getTokenList();
expect(tokenList).to.have.lengthOf.above(0);
});
});
});

context('Test build', function () {
const testCases: LogicTestCaseWithChainId<SwapTokenLogicFields, SwapTokenLogicOptions>[] = [
{
chainId: common.ChainId.metis,
fields: {
input: new common.TokenAmount(metisTokens.WETH, '1'),
output: new common.TokenAmount(metisTokens.USDC, '0'),
slippage: 100,
},
options: { account: '0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa' },
},
{
chainId: common.ChainId.metis,
fields: {
input: new common.TokenAmount(metisTokens.METIS, '1'),
output: new common.TokenAmount(metisTokens.USDC, '0'),
slippage: 100,
},
options: { account: '0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa' },
},
{
chainId: common.ChainId.metis,
fields: {
input: new common.TokenAmount(metisTokens.USDC, '1'),
output: new common.TokenAmount(metisTokens.METIS, '0'),
slippage: 100,
},
options: { account: '0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa' },
},
];

testCases.forEach(({ chainId, fields, options }) => {
it(`${fields.input.token.symbol} to ${fields.output.token.symbol}`, async function () {
const logic = new SwapTokenLogic(chainId);
const routerLogic = await logic.build(fields, options);
const { input } = fields;

expect(routerLogic.to).to.eq(getExchangeAddress(chainId));
expect(utils.isBytesLike(routerLogic.data)).to.be.true;
expect(routerLogic.inputs[0].balanceBps).to.eq(core.BPS_NOT_USED);
expect(routerLogic.inputs[0].amountOrOffset).to.eq(input.amountWei);
expect(routerLogic.approveTo).to.eq(getExchangeAddress(chainId));
expect(routerLogic.callback).to.eq(constants.AddressZero);
});
});
});
});
97 changes: 97 additions & 0 deletions src/logics/openocean-v2/logic.swap-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { axios } from 'src/utils';
import * as common from '@protocolink/common';
import * as core from '@protocolink/core';
import { getChainId, getExchangeAddress, getGasPrice, supportedChainIds } from './configs';
import { slippageToOpenOcean, slippageToProtocolink } from './slippage';

export type SwapTokenLogicTokenList = common.Token[];

export type SwapTokenLogicParams = core.TokenToTokenExactInParams<{
slippage?: number;
disabledDexIds?: string;
}>;

export type SwapTokenLogicFields = core.TokenToTokenExactInFields<{
slippage?: number;
disabledDexIds?: string;
}>;

export type SwapTokenLogicOptions = Pick<core.GlobalOptions, 'account'>;

@core.LogicDefinitionDecorator()
export class SwapTokenLogic
extends core.Logic
implements core.LogicTokenListInterface, core.LogicOracleInterface, core.LogicBuilderInterface
{
static readonly supportedChainIds = supportedChainIds;

async getTokenList() {
const resp = await axios.get(`https://open-api.openocean.finance/v3/${this.chainId.toString()}/tokenList`);
const tokens = resp.data.data;
const tokenList: SwapTokenLogicTokenList = [];
for (const { chain, address, decimals, symbol, name } of tokens) {
tokenList.push(new common.Token(getChainId(chain), address, decimals, symbol, name));
}
return tokenList;
}

// If you wish to exclude quotes from a specific DEX, you can include the corresponding DEX ID
// in the 'disabledDexIds' parameter. You can retrieve the DEX IDs from the following API:
// https://open-api.openocean.finance/v3/{chainId}/dexList
async quote(params: SwapTokenLogicParams) {
const { input, tokenOut, disabledDexIds } = params;
const gasPrice = getGasPrice(this.chainId);
let slippage = slippageToOpenOcean(params.slippage ?? 0);

const client = axios.create({ baseURL: `https://open-api.openocean.finance/v3/${this.chainId.toString()}` });
const resp = await client.get(`/quote`, {
params: {
inTokenAddress: input.token.address,
outTokenAddress: tokenOut.address,
amount: input.amount,
gasPrice,
slippage,
disabledDexIds,
},
});

slippage = slippageToProtocolink(slippage);

const { outAmount } = resp.data.data;
const output = new common.TokenAmount(tokenOut).setWei(outAmount);
return { input, output, slippage, disabledDexIds };
}

// Different gas_price will lead to different routes.
// This is due to that OpenOcean calculates the best overall return.
// The best overall return = out_value - tx cost and the tx_cost = gas_used & gas_price
async build(fields: SwapTokenLogicFields, options: SwapTokenLogicOptions) {
const { input, output, disabledDexIds } = fields;
const { account } = options;
const gasPrice = getGasPrice(this.chainId);
const agent = await this.calcAgent(account);
const slippage = slippageToOpenOcean(fields.slippage ?? 0);

const client = axios.create({ baseURL: `https://open-api.openocean.finance/v3/${this.chainId.toString()}` });
const resp = await client.get(`/swap_quote`, {
params: {
inTokenAddress: input.token.address,
outTokenAddress: output.token.address,
amount: input.amount,
gasPrice,
slippage,
account: agent,
disabledDexIds,
},
});

const { to, data } = resp.data.data;
const approveTo = getExchangeAddress(this.chainId);
const tokenInput = new common.TokenAmount(
new common.Token(this.chainId, input.token.address, input.token.decimals, input.token.symbol, input.token.name),
input.amount
);
const inputs = [core.newLogicInput({ input: tokenInput })];
return core.newLogic({ to, data, inputs, approveTo });
}
}
54 changes: 54 additions & 0 deletions src/logics/openocean-v2/slippage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { expect } from 'chai';
import { slippageToOpenOcean, slippageToProtocolink } from './slippage';

describe('Test slippageToOpenOcean', function () {
const testCases = [
{
title: 'in range integer',
slippage: 100,
expected: 1,
},
{
title: 'in range floating number',
slippage: 150,
expected: 1.5,
},
{
title: 'out of range minimal',
slippage: 0,
expected: 0.05,
},
{
title: 'out of range maximal',
slippage: 10000,
expected: 50,
},
];

testCases.forEach(({ title, slippage, expected }) => {
it(title, function () {
expect(slippageToOpenOcean(slippage) === expected).to.be.true;
});
});
});

describe('Test slippageToProtocolink', function () {
const testCases = [
{
title: 'in range integer',
slippage: 1,
expected: 100,
},
{
title: 'in range floating number',
slippage: 1.5,
expected: 150,
},
];

testCases.forEach(({ title, slippage, expected }) => {
it(title, function () {
expect(slippageToProtocolink(slippage) === expected).to.be.true;
});
});
});
13 changes: 13 additions & 0 deletions src/logics/openocean-v2/slippage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// TODO: test floating point
export function slippageToOpenOcean(slippage: number) {
const min = 0.05;
const max = 50;
const normalizedSlippage = slippage / 100;
if (normalizedSlippage < min) return min;
if (normalizedSlippage > max) return max;
return normalizedSlippage;
}

export function slippageToProtocolink(slippage: number) {
return slippage * 100;
}
30 changes: 30 additions & 0 deletions src/logics/openocean-v2/tokens/data/metis.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"METIS": {
"chainId": 1088,
"address": "0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000",
"decimals": 18,
"symbol": "METIS",
"name": "Metis"
},
"USDC": {
"chainId": 1088,
"address": "0xEA32A96608495e54156Ae48931A7c20f0dcc1a21",
"decimals": 6,
"symbol": "m.USDC",
"name": "USDC Token"
},
"WETH": {
"chainId": 1,
"address": "0x420000000000000000000000000000000000000a",
"decimals": 18,
"symbol": "WETH",
"name": "Wrapped Ether"
},
"DAI": {
"chainId": 1088,
"address": "0x4c078361FC9BbB78DF910800A991C7c3DD2F6ce0",
"decimals": 18,
"symbol": "m.DAI",
"name": "DAI Token"
}
}
6 changes: 6 additions & 0 deletions src/logics/openocean-v2/tokens/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as common from '@protocolink/common';
import metisTokensJSON from './data/metis.json';

type MetisTokenSymbols = keyof typeof metisTokensJSON;

export const metisTokens = common.toTokenMap<MetisTokenSymbols>(metisTokensJSON);
Loading

0 comments on commit 4f45a1e

Please sign in to comment.