From 9ae655fc8b7051208b0ecf91c2b946217db7e77b Mon Sep 17 00:00:00 2001 From: chebiN <17909998+seguido@users.noreply.github.com> Date: Thu, 12 Sep 2024 17:06:57 -0300 Subject: [PATCH 1/6] odos zap support --- src/api/zap/api/odos/OdosApi.ts | 122 ++++++++++++++++++ src/api/zap/api/odos/RateLimitedOdosApi.ts | 24 ++++ src/api/zap/api/odos/index.ts | 53 ++++++++ src/api/zap/api/odos/types.ts | 95 ++++++++++++++ src/api/zap/proxy/odos.ts | 60 +++++++++ .../zap/swap/providers/OdosSwapProvider.ts | 58 +++++++++ src/api/zap/swap/providers/index.ts | 2 + src/router.js | 3 + src/utils/secrets.ts | 2 +- 9 files changed, 418 insertions(+), 1 deletion(-) create mode 100644 src/api/zap/api/odos/OdosApi.ts create mode 100644 src/api/zap/api/odos/RateLimitedOdosApi.ts create mode 100644 src/api/zap/api/odos/index.ts create mode 100644 src/api/zap/api/odos/types.ts create mode 100644 src/api/zap/proxy/odos.ts create mode 100644 src/api/zap/swap/providers/OdosSwapProvider.ts diff --git a/src/api/zap/api/odos/OdosApi.ts b/src/api/zap/api/odos/OdosApi.ts new file mode 100644 index 000000000..7aeadef85 --- /dev/null +++ b/src/api/zap/api/odos/OdosApi.ts @@ -0,0 +1,122 @@ +import { URLSearchParams } from 'url'; +import { + IOdosApi, + isOdosErrorResponse, + QuoteRequest, + QuoteResponse, + SwapRequest, + SwapResponse, +} from './types'; +import { redactSecrets } from '../../../../utils/secrets'; +import { ApiResponse, isErrorApiResponse } from '../common'; + +export class OdosApi implements IOdosApi { + constructor(protected readonly baseUrl: string, protected readonly chainId: number) {} + + protected buildUrl(path: string, request?: T) { + const params = request ? new URLSearchParams(request).toString() : ''; + return params ? `${this.baseUrl}${path}?${params}` : `${this.baseUrl}${path}`; + } + + protected withChainId(request?: Record): Record { + return { + ...request, + chainId: this.chainId, + }; + } + + protected buildHeaders(additionalHeaders?: Record): Record { + return { + Accept: 'application/json,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.9', + 'User-Agent': 'BeefyApi', + ...additionalHeaders, + }; + } + + protected async doPost( + path: string, + request: Record + ): Promise> { + const url = this.buildUrl(path); + + const response = await fetch(url, { + method: 'POST', + headers: this.buildHeaders({ + 'Content-Type': 'application/json', + }), + body: JSON.stringify(this.withChainId(request)), + }); + + return this.handleResponse(response); + } + + protected async post( + path: string, + request: Record + ): Promise> { + return this.doPost(path, request); + } + + protected async priorityPost( + path: string, + request: Record + ): Promise> { + return this.doPost(path, request); + } + + protected async handleResponse( + response: Response + ): Promise> { + if (response.headers.get('content-type')?.includes('application/json')) { + const body = await response.json(); + + if (response.status === 200) { + return { + code: 200, + data: body as ResponseType, + }; + } + + if (isOdosErrorResponse(body)) { + return { + code: response.status === 200 ? 500 : response.status, + message: redactSecrets(body.detail), + }; + } + } + + return { + code: response.status === 200 ? 500 : response.status, + message: response.status === 200 ? 'upstream response not json' : redactSecrets(response.statusText), + }; + } + + async postQuote(request: QuoteRequest): Promise { + const response = await this.post('/sor/quote/v2', request); + + if (isErrorApiResponse(response)) { + throw new Error(`Error fetching quote: ${response.code} ${response.message}`); + } + + return response.data; + } + + async postSwap(request: SwapRequest): Promise { + const response = await this.post('/sor/assemble', request); + + if (isErrorApiResponse(response)) { + throw new Error(`Error fetching swap: ${response.message}`); + } + + return response.data; + } + + async postProxiedQuote(request: QuoteRequest): Promise> { + return await this.priorityPost('/sor/quote/v2', request); + } + + async postProxiedSwap(request: SwapRequest): Promise> { + return await this.priorityPost('/sor/assemble', request); + } +} diff --git a/src/api/zap/api/odos/RateLimitedOdosApi.ts b/src/api/zap/api/odos/RateLimitedOdosApi.ts new file mode 100644 index 000000000..956d78bf6 --- /dev/null +++ b/src/api/zap/api/odos/RateLimitedOdosApi.ts @@ -0,0 +1,24 @@ +import { OdosApi } from './OdosApi'; +import PQueue from 'p-queue'; +import { ApiResponse } from '../common'; + +export class RateLimitedOdosApi extends OdosApi { + constructor(baseUrl: string, chainId: number, protected readonly queue: PQueue) { + super(baseUrl, chainId); + } + + protected async post( + path: string, + request?: Record + ): Promise> { + return this.queue.add(() => super.post(path, request)); + } + + protected async priorityPost( + path: string, + request?: Record + ): Promise> { + // Rate limit, but higher priority than normal post, as these are used for app api proxy + return this.queue.add(() => super.priorityPost(path, request), { priority: 2 }); + } +} diff --git a/src/api/zap/api/odos/index.ts b/src/api/zap/api/odos/index.ts new file mode 100644 index 000000000..e0b044221 --- /dev/null +++ b/src/api/zap/api/odos/index.ts @@ -0,0 +1,53 @@ +import PQueue from 'p-queue'; +import { RateLimitedOdosApi } from './RateLimitedOdosApi'; +import { AnyChain, ApiChain, toApiChain } from '../../../../utils/chain'; +import { IOdosApi } from './types'; + +// Configure rate limiting +const API_QUEUE_CONFIG = { + concurrency: 2, + intervalCap: 1, // 1 per 400ms is 2.5 RPS + interval: 400, + carryoverConcurrencyCount: true, + autoStart: true, + timeout: 30 * 1000, + throwOnTimeout: true, +}; + +// @see https://docs.odos.xyz/api/endpoints/#/Info/get_chain_ids_info_chains_get +export const supportedChains: Partial> = { + ethereum: 1, + zksync: 324, + base: 8453, + mantle: 5000, + polygon: 137, + optimism: 10, + mode: 34443, + avax: 43114, + linea: 59144, + arbitrum: 42161, + bsc: 56, + fantom: 250, +} as const; + +const swapApiByChain: Partial> = {}; +let swapApiQueue: PQueue | undefined; + +export function getOdosApi(chain: AnyChain): IOdosApi { + const apiChain = toApiChain(chain); + const odosChain = supportedChains[apiChain]; + if (!odosChain) { + throw new Error(`Odos api is not supported on ${apiChain}`); + } + + if (!swapApiByChain[apiChain]) { + if (!swapApiQueue) { + swapApiQueue = new PQueue(API_QUEUE_CONFIG); + } + + const baseUrl = `https://api.odos.xyz`; + swapApiByChain[apiChain] = new RateLimitedOdosApi(baseUrl, odosChain, swapApiQueue); + } + + return swapApiByChain[apiChain]; +} diff --git a/src/api/zap/api/odos/types.ts b/src/api/zap/api/odos/types.ts new file mode 100644 index 000000000..f905bba08 --- /dev/null +++ b/src/api/zap/api/odos/types.ts @@ -0,0 +1,95 @@ +import { ApiResponse } from '../common'; + +export type QuoteRequest = { + inputTokens: Array<{ + tokenAddress: string; + amount: string; + }>; + outputTokens: Array<{ + tokenAddress: string; + proportion: number; + }>; + gasPrice?: string; + userAddr?: string; + slippageLimitPercent?: number; + sourceWhitelist?: Array; + sourceBlacklist?: Array; + poolBlacklist?: Array; + referralCode?: number; +}; + +export type QuoteResponse = { + inTokens: Array; + outTokens: Array; + inAmounts: Array; + outAmounts: Array; + gasEstimate: number; + dataGasEstimate: number; + gweiPerGas: number; + gasEstimateValue: number; + inValues: Array; // usd + outValues: Array; // usd + netOutValue: number; + priceImpact: number; + percentDiff: number; + partnerFeePercent: number; + pathId: string; + blockNumber: number; +}; + +export type SwapRequest = { + userAddr: string; + pathId: string; + receiver?: string; + simulate?: boolean; +}; + +export type SwapResponse = { + deprecated?: string; + blockNumber: number; + gasEstimate: number; + gasEstimateValue: number; + inputTokens: Array<{ + tokenAddress: string; + amount: string; + }>; + outputTokens: Array<{ + tokenAddress: string; + amount: string; + }>; + netOutValue: number; + transaction: { + gas: number; + gasPrice: number; + value: string; + to: string; + from: string; + data: string; + nonce: number; + chainId: number; + }; + simulation?: { + isSuccess: boolean; + amountsOut: Array; + gasEstimate: number; + simulationError: { + type: string; + errorMessage: string; + }; + }; +}; + +export type OdosErrorResponse = { + detail: string; +}; + +export function isOdosErrorResponse(obj: unknown): obj is OdosErrorResponse { + return obj && typeof obj === 'object' && 'detail' in obj; +} + +export interface IOdosApi { + postQuote(request: QuoteRequest): Promise; + postSwap(request: SwapRequest): Promise; + postProxiedQuote(request: QuoteRequest): Promise>; + postProxiedSwap(request: SwapRequest): Promise>; +} diff --git a/src/api/zap/proxy/odos.ts b/src/api/zap/proxy/odos.ts new file mode 100644 index 000000000..72617fde6 --- /dev/null +++ b/src/api/zap/proxy/odos.ts @@ -0,0 +1,60 @@ +import Koa from 'koa'; +import { AnyChain } from '../../../utils/chain'; +import { redactSecrets } from '../../../utils/secrets'; +import { ApiResponse, isSuccessApiResponse } from '../api/common'; +import { getOdosApi } from '../api/odos'; +import { QuoteRequest, QuoteResponse, SwapRequest, SwapResponse } from '../api/odos/types'; +import { setNoCacheHeaders } from './common'; + +const postProxiedQuote = async ( + request: QuoteRequest, + chain: AnyChain +): Promise> => { + try { + const api = getOdosApi(chain); + return await api.postProxiedQuote(request); + } catch (err) { + return { + code: 500, + message: redactSecrets(err.message || 'Unknown error'), + }; + } +}; + +export async function proxyOdosQuote(ctx: Koa.Context) { + const start = Date.now(); + const chain = ctx.params.chainId; + const requestObject: QuoteRequest = ctx.request['body'] as any; + const proxiedQuote = await postProxiedQuote(requestObject, chain); + if (isSuccessApiResponse(proxiedQuote)) { + console.log(`proxyOdosQuote took ${(Date.now() - start) / 1000}s on ${chain}`); + } + setNoCacheHeaders(ctx); + ctx.status = proxiedQuote.code; + ctx.body = isSuccessApiResponse(proxiedQuote) ? proxiedQuote.data : proxiedQuote.message; +} + +const postProxiedSwap = async (request: SwapRequest, chain: AnyChain): Promise> => { + try { + const api = getOdosApi(chain); + return await api.postProxiedSwap(request); + } catch (err) { + return { + code: 500, + message: redactSecrets(err.message || 'Unknown error'), + }; + } +}; + +export async function proxyOdosSwap(ctx: Koa.Context) { + const start = Date.now(); + const chain = ctx.params.chainId; + const requestObject: SwapRequest = ctx.request['body'] as any; + const proxiedSwap = await postProxiedSwap(requestObject, chain); + if (isSuccessApiResponse(proxiedSwap)) { + console.log(`proxyOdosSwap took ${(Date.now() - start) / 1000}s on ${chain}`); + } + setNoCacheHeaders(ctx); + ctx.status = proxiedSwap.code; + ctx.body = isSuccessApiResponse(proxiedSwap) ? proxiedSwap.data : proxiedSwap.message; +} diff --git a/src/api/zap/swap/providers/OdosSwapProvider.ts b/src/api/zap/swap/providers/OdosSwapProvider.ts new file mode 100644 index 000000000..66886e4cf --- /dev/null +++ b/src/api/zap/swap/providers/OdosSwapProvider.ts @@ -0,0 +1,58 @@ +import { ISwapProvider, SwapRequest, SwapResponse } from './ISwapProvider'; +import { getOdosApi, supportedChains } from '../../api/odos'; +import { fromWeiString, toWeiString } from '../../../../utils/big-number'; +import { ApiChain } from '../../../../utils/chain'; +import { isResultFulfilled } from '../../../../utils/promise'; + +export class OdosSwapProvider implements ISwapProvider { + public readonly id = 'odos'; + + supportsChain(chain: ApiChain): boolean { + return !!supportedChains[chain]; + } + + async quotes(swaps: SwapRequest[]): Promise { + if (swaps.length === 0) { + return []; + } + + const chainId = swaps[0].from.chainId; + const api = getOdosApi(chainId); + const results = await Promise.allSettled( + swaps.map(swap => + api.postQuote({ + inputTokens: [ + { + tokenAddress: swap.from.address, + amount: toWeiString(swap.fromAmount, swap.from.decimals), + }, + ], + outputTokens: [ + { + tokenAddress: swap.to.address, + proportion: 1, + }, + ], + }) + ) + ); + + return results.map((result, index) => { + if (!isResultFulfilled(result)) { + return { + from: swaps[index].from, + fromAmount: swaps[index].fromAmount, + to: swaps[index].to, + error: result.reason?.message || 'Unknown request error', + }; + } + + return { + from: swaps[index].from, + fromAmount: swaps[index].fromAmount, + to: swaps[index].to, + toAmount: fromWeiString(result.value.outAmounts[0], swaps[index].to.decimals), + }; + }); + } +} diff --git a/src/api/zap/swap/providers/index.ts b/src/api/zap/swap/providers/index.ts index adb6613c8..676c1d1cc 100644 --- a/src/api/zap/swap/providers/index.ts +++ b/src/api/zap/swap/providers/index.ts @@ -1,10 +1,12 @@ import { OneInchSwapProvider } from './OneInchSwapProvider'; import { KyberSwapProvider } from './KyberSwapProvider'; import { ISwapProvider } from './ISwapProvider'; +import { OdosSwapProvider } from './OdosSwapProvider'; export const providersById = { 'one-inch': new OneInchSwapProvider(), kyber: new KyberSwapProvider(), + odos: new OdosSwapProvider(), } as const satisfies Record; export type ProviderId = keyof typeof providersById; diff --git a/src/router.js b/src/router.js index 6f0699db3..310c1fad5 100644 --- a/src/router.js +++ b/src/router.js @@ -32,6 +32,7 @@ const { handleOffChainRewardsActiveForChain, } = require('./api/offchain-rewards'); const { pointStructures } = require('./api/points'); +const { proxyOdosQuote, proxyOdosSwap } = require('./api/zap/proxy/odos'); router.get('/validator-performance', validatorPerformance); @@ -99,6 +100,8 @@ router.get('/zap/providers/oneinch/:chainId/quote', proxyOneInchQuote); router.get('/zap/providers/oneinch/:chainId/swap', proxyOneInchSwap); router.get('/zap/providers/kyber/:chainId/quote', proxyKyberQuote); router.post('/zap/providers/kyber/:chainId/swap', proxyKyberSwap); +router.post('/zap/providers/odos/:chainId/quote', proxyOdosQuote); +router.post('/zap/providers/odos/:chainId/swap', proxyOdosSwap); router.get('/articles', getArticles); router.get('/articles/latest', getLatestArticle); diff --git a/src/utils/secrets.ts b/src/utils/secrets.ts index f33f633a4..ec5d71074 100644 --- a/src/utils/secrets.ts +++ b/src/utils/secrets.ts @@ -1,7 +1,7 @@ import { pick, pickBy } from 'lodash'; import escapeStringRegexp from 'escape-string-regexp'; -const SECRET_ENV_KEYS = ['ONE_INCH_API', 'KYBER_API']; +const SECRET_ENV_KEYS = ['ONE_INCH_API', 'KYBER_API', 'ODOS']; const SECRET_ENV_SUFFIXES = ['_RPC', '_KEY', '_TOKEN', '_URL']; const SECRETS: Record = { From 2e555e7186017306b97b15abb74c2f0245483afc Mon Sep 17 00:00:00 2001 From: chebiN <17909998+seguido@users.noreply.github.com> Date: Thu, 19 Sep 2024 13:38:48 -0300 Subject: [PATCH 2/6] add arbitrum treasurySwapper address --- .../src/address-book/arbitrum/platforms/beefyfinance.ts | 1 + packages/address-book/src/types/beefyfinance.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/address-book/src/address-book/arbitrum/platforms/beefyfinance.ts b/packages/address-book/src/address-book/arbitrum/platforms/beefyfinance.ts index cc6522e53..5236e067b 100644 --- a/packages/address-book/src/address-book/arbitrum/platforms/beefyfinance.ts +++ b/packages/address-book/src/address-book/arbitrum/platforms/beefyfinance.ts @@ -21,6 +21,7 @@ export const beefyfinance = { strategyFactory: '0xeF7746F16e511242e25Ad4FF9732bb5fC35EAB50', zap: '0xf49F7bB6F4F50d272A0914a671895c4384696E5A', zapTokenManager: '0x3395BDAE49853Bc7Ab9377d2A93f42BC3A18680e', + treasurySwapper: '0x8Ccf8606ccf0Aff9937B68e0297967e257eB148b', /// CLM Contracts clmFactory: '0xD41Ce2c0a0596635FC09BDe2C35946a984b8cB7A', diff --git a/packages/address-book/src/types/beefyfinance.ts b/packages/address-book/src/types/beefyfinance.ts index cc9d62783..e570c1ff1 100644 --- a/packages/address-book/src/types/beefyfinance.ts +++ b/packages/address-book/src/types/beefyfinance.ts @@ -18,6 +18,7 @@ export interface BeefyFinance { vaultFactory?: string; zap?: string; zapTokenManager?: string; + treasurySwapper?: string; /// BIFI Token Contracts mooBifiLockbox?: string; From 5589e1b5db1713ba541af3a0a9754c198d8bee4d Mon Sep 17 00:00:00 2001 From: chebiN <17909998+seguido@users.noreply.github.com> Date: Thu, 19 Sep 2024 13:44:15 -0300 Subject: [PATCH 3/6] add fee data to zap requests --- src/api/zap/api/kyber/KyberApi.ts | 36 ++++++++++++---- src/api/zap/api/kyber/RateLimitedKyberApi.ts | 5 ++- src/api/zap/api/kyber/index.ts | 2 +- src/api/zap/api/odos/OdosApi.ts | 20 ++++++--- src/api/zap/api/one-inch/OneInchSwapApi.ts | 42 ++++++++++++++++--- .../api/one-inch/RateLimitedOneInchSwapApi.ts | 5 ++- src/api/zap/api/one-inch/index.ts | 2 +- src/utils/secrets.ts | 2 +- 8 files changed, 90 insertions(+), 24 deletions(-) diff --git a/src/api/zap/api/kyber/KyberApi.ts b/src/api/zap/api/kyber/KyberApi.ts index bc2afcf9e..2e06fb82c 100644 --- a/src/api/zap/api/kyber/KyberApi.ts +++ b/src/api/zap/api/kyber/KyberApi.ts @@ -14,18 +14,27 @@ import { import { mapValues, omitBy } from 'lodash'; import { redactSecrets } from '../../../../utils/secrets'; import { ApiResponse, isErrorApiResponse } from '../common'; +import { ApiChain } from '../../../../utils/chain'; +import { addressBook } from '../../../../../packages/address-book/src/address-book'; export class KyberApi implements IKyberApi { - constructor(protected readonly baseUrl: string, protected readonly clientId: string) {} + readonly feeReceiver: string; + readonly ZAP_FEE = 0.0005; + constructor(protected readonly baseUrl: string, protected readonly clientId: string, chain: ApiChain) { + const beefyPlatform = addressBook[chain].platforms.beefyfinance; + if (!beefyPlatform) { + throw new Error(`No Beefy platform found for chain ${chain}`); + } + this.feeReceiver = + beefyPlatform.treasurySwapper || beefyPlatform.treasuryMultisig || beefyPlatform.treasury; + } protected buildUrl(path: string, request?: T) { const params = request ? new URLSearchParams(request).toString() : ''; return params ? `${this.baseUrl}${path}?${params}` : `${this.baseUrl}${path}`; } - protected toStringDict( - obj: Record - ): Record { + protected toStringDict(obj: Record): Record { return mapValues( omitBy(obj, v => v === undefined), v => (Array.isArray(v) ? v.join(',') : String(v)) @@ -49,6 +58,20 @@ export class KyberApi implements IKyberApi { }; } + protected withFeeReceiver( + request?: Record + ): Record { + return this.feeReceiver && (this.ZAP_FEE || 0) > 0 + ? { + ...request, + feeAmount: (this.ZAP_FEE * 10000).toString(10), // *10000 to bps + isInBps: true, + chargeFeeBy: 'currency_in', + feeReceiver: this.feeReceiver, + } + : request; + } + protected async doGet( path: string, request?: Record @@ -131,8 +154,7 @@ export class KyberApi implements IKyberApi { return { code: response.status === 200 ? 500 : response.status, - message: - response.status === 200 ? 'upstream response not json' : redactSecrets(response.statusText), + message: response.status === 200 ? 'upstream response not json' : redactSecrets(response.statusText), }; } @@ -157,7 +179,7 @@ export class KyberApi implements IKyberApi { } async getProxiedQuote(request: QuoteRequest): Promise> { - return await this.priorityGet('/routes', this.toStringDict(request)); + return await this.priorityGet('/routes', this.toStringDict(this.withFeeReceiver(request))); } async postProxiedSwap(request: SwapRequest): Promise> { diff --git a/src/api/zap/api/kyber/RateLimitedKyberApi.ts b/src/api/zap/api/kyber/RateLimitedKyberApi.ts index 432167749..45d892e52 100644 --- a/src/api/zap/api/kyber/RateLimitedKyberApi.ts +++ b/src/api/zap/api/kyber/RateLimitedKyberApi.ts @@ -1,10 +1,11 @@ import { KyberApi } from './KyberApi'; import PQueue from 'p-queue'; import { ApiResponse } from '../common'; +import { ApiChain } from '../../../../utils/chain'; export class RateLimitedKyberApi extends KyberApi { - constructor(baseUrl: string, clientId: string, protected readonly queue: PQueue) { - super(baseUrl, clientId); + constructor(baseUrl: string, clientId: string, protected readonly queue: PQueue, chain: ApiChain) { + super(baseUrl, clientId, chain); } protected async get( diff --git a/src/api/zap/api/kyber/index.ts b/src/api/zap/api/kyber/index.ts index 9e5709816..85d0a2e6f 100644 --- a/src/api/zap/api/kyber/index.ts +++ b/src/api/zap/api/kyber/index.ts @@ -53,7 +53,7 @@ export function getKyberApi(chain: AnyChain): IKyberApi { throw new Error(`KYBER_CLIENT_ID env variable is not set`); } - swapApiByChain[apiChain] = new RateLimitedKyberApi(baseUrl, clientId, swapApiQueue); + swapApiByChain[apiChain] = new RateLimitedKyberApi(baseUrl, clientId, swapApiQueue, apiChain); } return swapApiByChain[apiChain]; diff --git a/src/api/zap/api/odos/OdosApi.ts b/src/api/zap/api/odos/OdosApi.ts index 7aeadef85..4bddf67cd 100644 --- a/src/api/zap/api/odos/OdosApi.ts +++ b/src/api/zap/api/odos/OdosApi.ts @@ -11,7 +11,10 @@ import { redactSecrets } from '../../../../utils/secrets'; import { ApiResponse, isErrorApiResponse } from '../common'; export class OdosApi implements IOdosApi { - constructor(protected readonly baseUrl: string, protected readonly chainId: number) {} + readonly referralCode: number; + constructor(protected readonly baseUrl: string, protected readonly chainId: number) { + this.referralCode = Number(process.env.ODOS_CODE || 0); + } protected buildUrl(path: string, request?: T) { const params = request ? new URLSearchParams(request).toString() : ''; @@ -25,6 +28,13 @@ export class OdosApi implements IOdosApi { }; } + protected withReferralCode(request?: Record): Record { + return { + ...request, + referralCode: this.referralCode, + }; + } + protected buildHeaders(additionalHeaders?: Record): Record { return { Accept: 'application/json,*/*;q=0.8', @@ -102,6 +112,10 @@ export class OdosApi implements IOdosApi { return response.data; } + async postProxiedQuote(request: QuoteRequest): Promise> { + return await this.priorityPost('/sor/quote/v2', this.withReferralCode(request)); + } + async postSwap(request: SwapRequest): Promise { const response = await this.post('/sor/assemble', request); @@ -112,10 +126,6 @@ export class OdosApi implements IOdosApi { return response.data; } - async postProxiedQuote(request: QuoteRequest): Promise> { - return await this.priorityPost('/sor/quote/v2', request); - } - async postProxiedSwap(request: SwapRequest): Promise> { return await this.priorityPost('/sor/assemble', request); } diff --git a/src/api/zap/api/one-inch/OneInchSwapApi.ts b/src/api/zap/api/one-inch/OneInchSwapApi.ts index f2fd4240c..7e8a8163b 100644 --- a/src/api/zap/api/one-inch/OneInchSwapApi.ts +++ b/src/api/zap/api/one-inch/OneInchSwapApi.ts @@ -10,9 +10,20 @@ import { import { mapValues, omitBy } from 'lodash'; import { redactSecrets } from '../../../../utils/secrets'; import { isErrorApiResponse, ApiResponse } from '../common'; +import { ApiChain } from '../../../../utils/chain'; +import { addressBook } from '../../../../../packages/address-book/src/address-book'; export class OneInchSwapApi implements IOneInchSwapApi { - constructor(protected readonly baseUrl: string, protected readonly apiKey: string) {} + readonly feeReceiver: string; + readonly ZAP_FEE = 0.0005; + constructor(protected readonly baseUrl: string, protected readonly apiKey: string, chain: ApiChain) { + const beefyPlatform = addressBook[chain].platforms.beefyfinance; + if (!beefyPlatform) { + throw new Error(`No Beefy platform found for chain ${chain}`); + } + this.feeReceiver = + beefyPlatform.treasurySwapper || beefyPlatform.treasuryMultisig || beefyPlatform.treasury; + } protected buildUrl(path: string, request?: T) { let queryString: string | undefined; @@ -62,8 +73,7 @@ export class OneInchSwapApi implements IOneInchSwapApi { return { code: response.status === 200 ? 500 : response.status, - message: - response.status === 200 ? 'upstream response not json' : redactSecrets(response.statusText), + message: response.status === 200 ? 'upstream response not json' : redactSecrets(response.statusText), }; } @@ -88,12 +98,34 @@ export class OneInchSwapApi implements IOneInchSwapApi { }; } + protected withFee( + request?: Record + ): Record { + return { + ...request, + fee: (this.ZAP_FEE * 100).toString(10), + }; + } + + protected withFeeReferrer( + request?: Record + ): Record { + return { + ...request, + fee: (this.ZAP_FEE * 100).toString(10), + referrer: this.feeReceiver, + }; + } + async getProxiedQuote(request: QuoteRequest): Promise> { - return await this.priorityGet('/quote', this.toStringDict(this.addRequiredParams(request))); + return await this.priorityGet('/quote', this.toStringDict(this.withFee(this.addRequiredParams(request)))); } async getProxiedSwap(request: SwapRequest): Promise> { - return await this.priorityGet('/swap', this.toStringDict(this.addRequiredParams(request))); + return await this.priorityGet( + '/swap', + this.toStringDict(this.withFeeReferrer(this.addRequiredParams(request))) + ); } async getQuote(request: QuoteRequest): Promise { diff --git a/src/api/zap/api/one-inch/RateLimitedOneInchSwapApi.ts b/src/api/zap/api/one-inch/RateLimitedOneInchSwapApi.ts index 919da0f42..6c7ef7449 100644 --- a/src/api/zap/api/one-inch/RateLimitedOneInchSwapApi.ts +++ b/src/api/zap/api/one-inch/RateLimitedOneInchSwapApi.ts @@ -1,10 +1,11 @@ import { OneInchSwapApi } from './OneInchSwapApi'; import PQueue from 'p-queue'; import { ApiResponse } from '../common'; +import { ApiChain } from '../../../../utils/chain'; export class RateLimitedOneInchSwapApi extends OneInchSwapApi { - constructor(baseUrl: string, apiKey: string, protected readonly queue: PQueue) { - super(baseUrl, apiKey); + constructor(baseUrl: string, apiKey: string, protected readonly queue: PQueue, chain: ApiChain) { + super(baseUrl, apiKey, chain); } protected async get( diff --git a/src/api/zap/api/one-inch/index.ts b/src/api/zap/api/one-inch/index.ts index b26ef0160..1ca5b06f9 100644 --- a/src/api/zap/api/one-inch/index.ts +++ b/src/api/zap/api/one-inch/index.ts @@ -49,7 +49,7 @@ export function getOneInchSwapApi(chain: AnyChain): IOneInchSwapApi { if (!apiKey) { throw new Error(`ONE_INCH_API_KEY env variable is not set`); } - swapApiByChain[apiChain] = new RateLimitedOneInchSwapApi(baseUrl, apiKey, swapApiQueue); + swapApiByChain[apiChain] = new RateLimitedOneInchSwapApi(baseUrl, apiKey, swapApiQueue, apiChain); } return swapApiByChain[apiChain]; diff --git a/src/utils/secrets.ts b/src/utils/secrets.ts index ec5d71074..a94f118b5 100644 --- a/src/utils/secrets.ts +++ b/src/utils/secrets.ts @@ -2,7 +2,7 @@ import { pick, pickBy } from 'lodash'; import escapeStringRegexp from 'escape-string-regexp'; const SECRET_ENV_KEYS = ['ONE_INCH_API', 'KYBER_API', 'ODOS']; -const SECRET_ENV_SUFFIXES = ['_RPC', '_KEY', '_TOKEN', '_URL']; +const SECRET_ENV_SUFFIXES = ['_RPC', '_KEY', '_TOKEN', '_URL', '_CODE']; const SECRETS: Record = { ...pick(process.env, SECRET_ENV_KEYS), From a30d84c7c3b53ae5f6e6c03493cef7f67801a28e Mon Sep 17 00:00:00 2001 From: chebiN <17909998+seguido@users.noreply.github.com> Date: Thu, 19 Sep 2024 13:47:23 -0300 Subject: [PATCH 4/6] allow for custom zap fee amounts per chain --- src/api/zap/api/kyber/KyberApi.ts | 5 ++++- src/api/zap/api/one-inch/OneInchSwapApi.ts | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/api/zap/api/kyber/KyberApi.ts b/src/api/zap/api/kyber/KyberApi.ts index 2e06fb82c..724c25120 100644 --- a/src/api/zap/api/kyber/KyberApi.ts +++ b/src/api/zap/api/kyber/KyberApi.ts @@ -17,9 +17,11 @@ import { ApiResponse, isErrorApiResponse } from '../common'; import { ApiChain } from '../../../../utils/chain'; import { addressBook } from '../../../../../packages/address-book/src/address-book'; +const DEFAULT_ZAP_FEE = 0.0005; +const customFeeConfig: Partial> = {}; export class KyberApi implements IKyberApi { readonly feeReceiver: string; - readonly ZAP_FEE = 0.0005; + readonly ZAP_FEE: number; constructor(protected readonly baseUrl: string, protected readonly clientId: string, chain: ApiChain) { const beefyPlatform = addressBook[chain].platforms.beefyfinance; if (!beefyPlatform) { @@ -27,6 +29,7 @@ export class KyberApi implements IKyberApi { } this.feeReceiver = beefyPlatform.treasurySwapper || beefyPlatform.treasuryMultisig || beefyPlatform.treasury; + this.ZAP_FEE = customFeeConfig[chain] || DEFAULT_ZAP_FEE; } protected buildUrl(path: string, request?: T) { diff --git a/src/api/zap/api/one-inch/OneInchSwapApi.ts b/src/api/zap/api/one-inch/OneInchSwapApi.ts index 7e8a8163b..c7d88e276 100644 --- a/src/api/zap/api/one-inch/OneInchSwapApi.ts +++ b/src/api/zap/api/one-inch/OneInchSwapApi.ts @@ -13,9 +13,12 @@ import { isErrorApiResponse, ApiResponse } from '../common'; import { ApiChain } from '../../../../utils/chain'; import { addressBook } from '../../../../../packages/address-book/src/address-book'; +const DEFAULT_ZAP_FEE = 0.0005; +const customFeeConfig: Partial> = {}; + export class OneInchSwapApi implements IOneInchSwapApi { readonly feeReceiver: string; - readonly ZAP_FEE = 0.0005; + readonly ZAP_FEE: number; constructor(protected readonly baseUrl: string, protected readonly apiKey: string, chain: ApiChain) { const beefyPlatform = addressBook[chain].platforms.beefyfinance; if (!beefyPlatform) { @@ -23,6 +26,7 @@ export class OneInchSwapApi implements IOneInchSwapApi { } this.feeReceiver = beefyPlatform.treasurySwapper || beefyPlatform.treasuryMultisig || beefyPlatform.treasury; + this.ZAP_FEE = customFeeConfig[chain] || DEFAULT_ZAP_FEE; } protected buildUrl(path: string, request?: T) { From 4ed6e63addb5ef8fb5ba481d8c099f3608d5a4fe Mon Sep 17 00:00:00 2001 From: chebiN <17909998+seguido@users.noreply.github.com> Date: Wed, 25 Sep 2024 10:19:39 -0300 Subject: [PATCH 5/6] single function for zap aggregator fee --- src/api/zap/api/kyber/KyberApi.ts | 17 +++++--------- src/api/zap/api/odos/OdosApi.ts | 11 ++++++++- src/api/zap/api/odos/RateLimitedOdosApi.ts | 5 ++-- src/api/zap/api/one-inch/OneInchSwapApi.ts | 22 +++++++++--------- src/api/zap/fees.ts | 27 ++++++++++++++++++++++ src/utils/secrets.ts | 4 ++-- 6 files changed, 59 insertions(+), 27 deletions(-) create mode 100644 src/api/zap/fees.ts diff --git a/src/api/zap/api/kyber/KyberApi.ts b/src/api/zap/api/kyber/KyberApi.ts index 724c25120..a173f9d78 100644 --- a/src/api/zap/api/kyber/KyberApi.ts +++ b/src/api/zap/api/kyber/KyberApi.ts @@ -6,30 +6,25 @@ import { KyberResponse, QuoteData, QuoteRequest, - QuoteResponse, SwapData, SwapRequest, - SwapResponse, } from './types'; import { mapValues, omitBy } from 'lodash'; import { redactSecrets } from '../../../../utils/secrets'; import { ApiResponse, isErrorApiResponse } from '../common'; import { ApiChain } from '../../../../utils/chain'; -import { addressBook } from '../../../../../packages/address-book/src/address-book'; +import { getZapProviderFee } from '../../fees'; -const DEFAULT_ZAP_FEE = 0.0005; -const customFeeConfig: Partial> = {}; export class KyberApi implements IKyberApi { readonly feeReceiver: string; readonly ZAP_FEE: number; constructor(protected readonly baseUrl: string, protected readonly clientId: string, chain: ApiChain) { - const beefyPlatform = addressBook[chain].platforms.beefyfinance; - if (!beefyPlatform) { - throw new Error(`No Beefy platform found for chain ${chain}`); + const feeData = getZapProviderFee('kyber', chain); + this.ZAP_FEE = feeData.value; + if (!feeData.receiver) { + throw new Error('No fee receiver found for Kyber on ' + chain); } - this.feeReceiver = - beefyPlatform.treasurySwapper || beefyPlatform.treasuryMultisig || beefyPlatform.treasury; - this.ZAP_FEE = customFeeConfig[chain] || DEFAULT_ZAP_FEE; + this.feeReceiver = feeData.receiver; } protected buildUrl(path: string, request?: T) { diff --git a/src/api/zap/api/odos/OdosApi.ts b/src/api/zap/api/odos/OdosApi.ts index 4bddf67cd..18e338712 100644 --- a/src/api/zap/api/odos/OdosApi.ts +++ b/src/api/zap/api/odos/OdosApi.ts @@ -9,11 +9,20 @@ import { } from './types'; import { redactSecrets } from '../../../../utils/secrets'; import { ApiResponse, isErrorApiResponse } from '../common'; +import { getZapProviderFee } from '../../fees'; +import { ApiChain, toChainId } from '../../../../utils/chain'; export class OdosApi implements IOdosApi { + readonly ZAP_FEE: number; readonly referralCode: number; - constructor(protected readonly baseUrl: string, protected readonly chainId: number) { + readonly chainId: number; + constructor(protected readonly baseUrl: string, protected readonly chain: ApiChain) { this.referralCode = Number(process.env.ODOS_CODE || 0); + this.ZAP_FEE = getZapProviderFee('odos', chain).value; + this.chainId = toChainId(chain); + if (this.chainId === undefined) { + throw new Error(`Invalid chain ${chain}`); + } } protected buildUrl(path: string, request?: T) { diff --git a/src/api/zap/api/odos/RateLimitedOdosApi.ts b/src/api/zap/api/odos/RateLimitedOdosApi.ts index 956d78bf6..7c5e02cea 100644 --- a/src/api/zap/api/odos/RateLimitedOdosApi.ts +++ b/src/api/zap/api/odos/RateLimitedOdosApi.ts @@ -1,10 +1,11 @@ import { OdosApi } from './OdosApi'; import PQueue from 'p-queue'; import { ApiResponse } from '../common'; +import { ApiChain } from '../../../../utils/chain'; export class RateLimitedOdosApi extends OdosApi { - constructor(baseUrl: string, chainId: number, protected readonly queue: PQueue) { - super(baseUrl, chainId); + constructor(baseUrl: string, chain: ApiChain, protected readonly queue: PQueue) { + super(baseUrl, chain); } protected async post( diff --git a/src/api/zap/api/one-inch/OneInchSwapApi.ts b/src/api/zap/api/one-inch/OneInchSwapApi.ts index c7d88e276..7c1f90112 100644 --- a/src/api/zap/api/one-inch/OneInchSwapApi.ts +++ b/src/api/zap/api/one-inch/OneInchSwapApi.ts @@ -11,22 +11,22 @@ import { mapValues, omitBy } from 'lodash'; import { redactSecrets } from '../../../../utils/secrets'; import { isErrorApiResponse, ApiResponse } from '../common'; import { ApiChain } from '../../../../utils/chain'; -import { addressBook } from '../../../../../packages/address-book/src/address-book'; - -const DEFAULT_ZAP_FEE = 0.0005; -const customFeeConfig: Partial> = {}; +import { getZapProviderFee } from '../../fees'; export class OneInchSwapApi implements IOneInchSwapApi { readonly feeReceiver: string; readonly ZAP_FEE: number; - constructor(protected readonly baseUrl: string, protected readonly apiKey: string, chain: ApiChain) { - const beefyPlatform = addressBook[chain].platforms.beefyfinance; - if (!beefyPlatform) { - throw new Error(`No Beefy platform found for chain ${chain}`); + constructor( + protected readonly baseUrl: string, + protected readonly apiKey: string, + protected readonly chain: ApiChain + ) { + const feeData = getZapProviderFee('one-inch', chain); + this.ZAP_FEE = feeData.value; + if (!feeData.receiver) { + throw new Error('No fee receiver found for OneInch on ' + chain); } - this.feeReceiver = - beefyPlatform.treasurySwapper || beefyPlatform.treasuryMultisig || beefyPlatform.treasury; - this.ZAP_FEE = customFeeConfig[chain] || DEFAULT_ZAP_FEE; + this.feeReceiver = feeData.receiver; } protected buildUrl(path: string, request?: T) { diff --git a/src/api/zap/fees.ts b/src/api/zap/fees.ts new file mode 100644 index 000000000..ac5163238 --- /dev/null +++ b/src/api/zap/fees.ts @@ -0,0 +1,27 @@ +import { addressBook } from '../../../packages/address-book/src/address-book'; +import { ApiChain } from '../../utils/chain'; +import { ProviderId } from './swap/providers'; + +export type ZapFee = { + value: number; + receiver?: string; +}; + +const DEFAULT_ZAP_FEE = 0.0005; +export const getZapProviderFee = (provider: ProviderId, chain: ApiChain) => { + if (provider === 'odos') + return { + value: DEFAULT_ZAP_FEE, + }; //It's static to the odos code config, we can't make it dynamic + + const beefyPlatform = addressBook[chain].platforms.beefyfinance; + if (!beefyPlatform) { + throw new Error('No Beefy Platform found for chain ' + chain); + } + + const receiver = beefyPlatform.treasurySwapper || beefyPlatform.treasuryMultisig || beefyPlatform.treasury; + return { + value: DEFAULT_ZAP_FEE, + receiver: receiver, + }; +}; diff --git a/src/utils/secrets.ts b/src/utils/secrets.ts index a94f118b5..76f713564 100644 --- a/src/utils/secrets.ts +++ b/src/utils/secrets.ts @@ -1,8 +1,8 @@ import { pick, pickBy } from 'lodash'; import escapeStringRegexp from 'escape-string-regexp'; -const SECRET_ENV_KEYS = ['ONE_INCH_API', 'KYBER_API', 'ODOS']; -const SECRET_ENV_SUFFIXES = ['_RPC', '_KEY', '_TOKEN', '_URL', '_CODE']; +const SECRET_ENV_KEYS = ['ONE_INCH_API', 'KYBER_API', 'ODOS_CODE']; +const SECRET_ENV_SUFFIXES = ['_RPC', '_KEY', '_TOKEN', '_URL']; const SECRETS: Record = { ...pick(process.env, SECRET_ENV_KEYS), From fd861a6df89160e849c89fe5fa9ce0beff4200fd Mon Sep 17 00:00:00 2001 From: chebiN <17909998+seguido@users.noreply.github.com> Date: Wed, 25 Sep 2024 10:20:02 -0300 Subject: [PATCH 6/6] typo --- src/api/zap/api/odos/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/zap/api/odos/index.ts b/src/api/zap/api/odos/index.ts index e0b044221..b1e359a0c 100644 --- a/src/api/zap/api/odos/index.ts +++ b/src/api/zap/api/odos/index.ts @@ -46,7 +46,7 @@ export function getOdosApi(chain: AnyChain): IOdosApi { } const baseUrl = `https://api.odos.xyz`; - swapApiByChain[apiChain] = new RateLimitedOdosApi(baseUrl, odosChain, swapApiQueue); + swapApiByChain[apiChain] = new RateLimitedOdosApi(baseUrl, apiChain, swapApiQueue); } return swapApiByChain[apiChain];