-
Notifications
You must be signed in to change notification settings - Fork 201
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* odos zap support * add arbitrum treasurySwapper address * add fee data to zap requests * allow for custom zap fee amounts per chain * single function for zap aggregator fee * typo
- Loading branch information
Showing
18 changed files
with
545 additions
and
21 deletions.
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
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
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
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,141 @@ | ||
import { URLSearchParams } from 'url'; | ||
import { | ||
IOdosApi, | ||
isOdosErrorResponse, | ||
QuoteRequest, | ||
QuoteResponse, | ||
SwapRequest, | ||
SwapResponse, | ||
} 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; | ||
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<T extends {}>(path: string, request?: T) { | ||
const params = request ? new URLSearchParams(request).toString() : ''; | ||
return params ? `${this.baseUrl}${path}?${params}` : `${this.baseUrl}${path}`; | ||
} | ||
|
||
protected withChainId(request?: Record<string, unknown>): Record<string, unknown> { | ||
return { | ||
...request, | ||
chainId: this.chainId, | ||
}; | ||
} | ||
|
||
protected withReferralCode(request?: Record<string, unknown>): Record<string, unknown> { | ||
return { | ||
...request, | ||
referralCode: this.referralCode, | ||
}; | ||
} | ||
|
||
protected buildHeaders(additionalHeaders?: Record<string, string>): Record<string, string> { | ||
return { | ||
Accept: 'application/json,*/*;q=0.8', | ||
'Accept-Language': 'en-US,en;q=0.9', | ||
'User-Agent': 'BeefyApi', | ||
...additionalHeaders, | ||
}; | ||
} | ||
|
||
protected async doPost<ResponseType extends object>( | ||
path: string, | ||
request: Record<string, unknown> | ||
): Promise<ApiResponse<ResponseType>> { | ||
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<ResponseType extends object>( | ||
path: string, | ||
request: Record<string, unknown> | ||
): Promise<ApiResponse<ResponseType>> { | ||
return this.doPost(path, request); | ||
} | ||
|
||
protected async priorityPost<ResponseType extends object>( | ||
path: string, | ||
request: Record<string, unknown> | ||
): Promise<ApiResponse<ResponseType>> { | ||
return this.doPost(path, request); | ||
} | ||
|
||
protected async handleResponse<ResponseType extends object>( | ||
response: Response | ||
): Promise<ApiResponse<ResponseType>> { | ||
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<QuoteResponse> { | ||
const response = await this.post<QuoteResponse>('/sor/quote/v2', request); | ||
|
||
if (isErrorApiResponse(response)) { | ||
throw new Error(`Error fetching quote: ${response.code} ${response.message}`); | ||
} | ||
|
||
return response.data; | ||
} | ||
|
||
async postProxiedQuote(request: QuoteRequest): Promise<ApiResponse<QuoteResponse>> { | ||
return await this.priorityPost<QuoteResponse>('/sor/quote/v2', this.withReferralCode(request)); | ||
} | ||
|
||
async postSwap(request: SwapRequest): Promise<SwapResponse> { | ||
const response = await this.post<SwapResponse>('/sor/assemble', request); | ||
|
||
if (isErrorApiResponse(response)) { | ||
throw new Error(`Error fetching swap: ${response.message}`); | ||
} | ||
|
||
return response.data; | ||
} | ||
|
||
async postProxiedSwap(request: SwapRequest): Promise<ApiResponse<SwapResponse>> { | ||
return await this.priorityPost<SwapResponse>('/sor/assemble', request); | ||
} | ||
} |
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,25 @@ | ||
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, chain: ApiChain, protected readonly queue: PQueue) { | ||
super(baseUrl, chain); | ||
} | ||
|
||
protected async post<ResponseType extends object>( | ||
path: string, | ||
request?: Record<string, string> | ||
): Promise<ApiResponse<ResponseType>> { | ||
return this.queue.add(() => super.post(path, request)); | ||
} | ||
|
||
protected async priorityPost<ResponseType extends object>( | ||
path: string, | ||
request?: Record<string, unknown> | ||
): Promise<ApiResponse<ResponseType>> { | ||
// 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 }); | ||
} | ||
} |
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,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<Record<ApiChain, number>> = { | ||
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<Record<ApiChain, IOdosApi>> = {}; | ||
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, apiChain, swapApiQueue); | ||
} | ||
|
||
return swapApiByChain[apiChain]; | ||
} |
Oops, something went wrong.