@yoroi/swap
Version:
The Swap package of Yoroi SDK
306 lines (270 loc) • 7.66 kB
text/typescript
import {fetchData, isLeft, isNonNullable, isRight} from '@yoroi/common'
import {Api, Chain, Left, Swap} from '@yoroi/types'
import {freeze} from 'immer'
import {
CancelResponse,
EstimateResponse,
LimitEstimateResponse,
LimitBuildResponse,
OrdersResponse,
ReverseEstimateResponse,
BuildResponse,
TokensResponse,
DexhunterApiConfig,
} from './types'
import {DexhunterProtocols, transformersMaker} from './transformers'
export const dexhunterApiMaker = (
config: DexhunterApiConfig,
): Readonly<Swap.Api> => {
const {
address,
partner,
network,
isPrimaryToken,
request = fetchData,
} = config
if (network !== Chain.Network.Mainnet)
return new Proxy(
{},
{
get() {
return () =>
freeze(
{
tag: 'left',
error: {
status: -3,
message: 'Dexhunter api only works on mainnet',
},
},
true,
)
},
},
) as Swap.Api
const baseUrl = baseUrls[network]
const headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
...(partner && {'X-Partner-Id': partner}),
}
const transformers = transformersMaker(config)
return freeze(
{
async tokens() {
const response = await request<TokensResponse>({
method: 'get',
url: `${baseUrl}${apiPaths.tokens}`,
headers,
})
if (isLeft(response)) return parseDhError(response)
return freeze(
{
tag: 'right',
value: {
status: response.value.status,
data: transformers.tokens.response(response.value.data),
},
},
true,
)
},
async orders() {
const response = await request<OrdersResponse>({
method: 'get',
url: `${baseUrl}${apiPaths.orders({address})}`,
headers,
})
if (isLeft(response)) return parseDhError(response)
return freeze(
{
tag: 'right',
value: {
status: response.value.status,
data: transformers.orders
.response(response.value.data)
.sort(
(
{lastUpdate: A, placedAt: A2},
{lastUpdate: B, placedAt: B2},
) => (B ?? B2 ?? 0) - (A ?? A2 ?? 0),
),
},
},
true,
)
},
/* istanbul ignore next */
async limitOptions({tokenIn, tokenOut}: Swap.LimitOptionsRequest) {
const estimateResponse = await this.estimate({
tokenIn,
tokenOut,
slippage: 0,
amountIn: 50,
})
if (isLeft(estimateResponse)) return parseDhError(estimateResponse)
const wantedPrice = estimateResponse.value.data.netPrice
const defaultProtocol = estimateResponse.value.data.splits[0]?.protocol
if (defaultProtocol === undefined)
return freeze<Left<Api.ResponseError>>(
{
tag: 'left',
error: {
status: -3,
message: 'Invalid state',
responseData: null,
},
},
true,
)
const options = (
await Promise.all(
DexhunterProtocols.map((protocol) =>
this.estimate({
tokenIn,
tokenOut,
slippage: 0,
amountIn: 50,
wantedPrice,
protocol,
}),
),
)
)
.filter(isRight)
.map((res) => {
const split = res.value.data.splits[0]
if (split === undefined) return null
const {protocol, initialPrice, batcherFee} = split
return {
protocol,
initialPrice,
batcherFee,
}
})
.filter(isNonNullable)
return freeze(
{
tag: 'right',
value: {
status: Api.HttpStatusCode.Ok,
data: {
defaultProtocol,
wantedPrice,
options,
},
},
},
true,
)
},
async estimate(body: Swap.EstimateRequest) {
const kind: 'estimate' | 'reverseEstimate' | 'limitEstimate' =
body.wantedPrice !== undefined
? 'limitEstimate'
: body.amountOut !== undefined
? 'reverseEstimate'
: 'estimate'
const response = await request<
EstimateResponse | ReverseEstimateResponse | LimitEstimateResponse
>({
method: 'post',
url: `${baseUrl}${apiPaths[kind]}`,
headers,
data: transformers[kind].request(body),
})
if (isLeft(response)) return parseDhError(response)
return freeze(
{
tag: 'right',
value: {
status: response.value.status,
data: transformers[kind].response(
response.value.data as any,
isPrimaryToken(body.tokenIn),
),
},
},
true,
)
},
async create(body: Swap.CreateRequest) {
const kind: 'build' | 'limitBuild' =
body.wantedPrice !== undefined ? 'limitBuild' : 'build'
const response = await request<BuildResponse | LimitBuildResponse>({
method: 'post',
url: `${baseUrl}${apiPaths[kind]}`,
headers,
data: transformers[kind].request(body),
})
if (isLeft(response)) return parseDhError(response)
return freeze(
{
tag: 'right',
value: {
status: response.value.status,
data: transformers[kind].response(
response.value.data as any,
isPrimaryToken(body.tokenIn),
),
},
},
true,
)
},
async cancel(body: Swap.CancelRequest) {
const response = await request<CancelResponse>({
method: 'post',
url: `${baseUrl}${apiPaths.cancel}`,
headers,
data: transformers.cancel.request(body),
})
if (isLeft(response)) return parseDhError(response)
return freeze(
{
tag: 'right',
value: {
status: response.value.status,
data: transformers.cancel.response(response.value.data),
},
},
true,
)
},
},
true,
)
}
const parseDhError = ({tag, error}: Left<Api.ResponseError>) =>
freeze<Left<Api.ResponseError>>(
{
tag,
error: {
...error,
message: JSON.stringify(
(error.responseData as any) ?? 'Dexhunter API error',
null,
2,
)
.replace(/^"/, '')
.replace(/"$/, ''),
},
},
true,
)
const baseUrls = freeze({
[Chain.Network.Mainnet]: 'https://api-us.dexhunterv3.app',
} as const)
const apiPaths = freeze(
{
tokens: '/swap/tokens',
orders: ({address}: {address: string}) => `/swap/orders/${address}`,
cancel: '/swap/cancel',
estimate: '/swap/estimate',
limitBuild: '/swap/limit/build',
limitEstimate: '/swap/limit/estimate',
reverseEstimate: '/swap/reverseEstimate',
build: '/swap/build',
} as const,
true,
)