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),