-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
395 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@protocolink/logics': patch | ||
--- | ||
|
||
support OpenOceanV2 on Metis |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './configs'; | ||
export * from './logic.swap-token'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
Oops, something went wrong.