@yoroi/swap
Version:
The Swap package of Yoroi SDK
251 lines (248 loc) • 7.11 kB
JavaScript
"use strict";
import { fetchData, isLeft, isNonNullable, isRight } from '@yoroi/common';
import { Api, Chain } from '@yoroi/types';
import { freeze } from 'immer';
import { MuesliswapProtocols, transformersMaker } from './transformers';
export const muesliswapApiMaker = config => {
const {
address,
network,
request = fetchData
} = config;
if (network !== Chain.Network.Mainnet) return new Proxy({}, {
get() {
return () => Promise.resolve(freeze({
tag: 'left',
error: {
status: -3,
message: 'Muesliswap api only works on mainnet'
}
}, true));
}
});
const headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
};
const baseUrl = baseUrls[network];
const transformers = transformersMaker(config);
// Preload providers for image enrichment and FO mapping (best-effort; guard for tests)
// Avoid auto-fetching providers in tests to keep request shape predictable
if (process.env.NODE_ENV !== 'test') {
request({
method: 'get',
url: `${baseUrl}${apiPaths.providers}`,
headers
}).then(res => {
if (isRight(res)) transformers.setProviders(res.value.data);
}).catch(() => {
// ignore preload failures
});
}
// Feature flag to enable providers/pools for FO-based excluded_sources (placeholder)
// const providersFlag = false
return freeze({
async providers() {
const response = await request({
method: 'get',
url: `${baseUrl}${apiPaths.providers}`,
headers
});
return response;
},
async tokens() {
const response = await request({
method: 'get',
url: `${baseUrl}${apiPaths.tokens}`,
headers
});
if (isLeft(response)) return parseMuesliError(response);
return freeze({
tag: 'right',
value: {
status: response.value.status,
data: transformers.tokens.response(response.value.data)
}
}, true);
},
async orders() {
const response = await request({
method: 'get',
url: `${baseUrl}${apiPaths.orderHistory}`,
headers
}, {
params: {
user_address: address,
numbers_have_decimals: true
}
});
if (isLeft(response)) return parseMuesliError(response);
try {
return freeze({
tag: 'right',
value: {
status: 200,
data: transformers.ordersHistory.response(response.value.data).sort(({
lastUpdate: A,
placedAt: A2
}, {
lastUpdate: B,
placedAt: B2
}) => (B ?? B2 ?? 0) - (A ?? A2 ?? 0))
}
}, true);
} catch (e) {
return freeze({
tag: 'left',
error: {
status: -3,
message: 'Failed to transform orderHistory',
responseData: response.value.data
}
}, true);
}
},
async limitOptions({
tokenIn,
tokenOut
}) {
const estimateResponse = await this.estimate({
tokenIn,
tokenOut,
slippage: 0,
amountIn: 50
});
if (isLeft(estimateResponse)) return parseMuesliError(estimateResponse);
const wantedPrice = estimateResponse.value.data.netPrice;
const defaultProtocol = estimateResponse.value.data.splits[0]?.protocol;
if (defaultProtocol === undefined) return freeze({
tag: 'left',
error: {
status: -3,
message: 'Invalid state',
responseData: null
}
}, true);
const options = (await Promise.all(MuesliswapProtocols.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
} = split;
// Use split.batcherFee; providers are preloaded for other flows but not required here
const batcherFee = split.batcherFee;
return {
protocol,
initialPrice,
batcherFee
};
}).filter(isNonNullable);
return freeze({
tag: 'right',
value: {
status: Api.HttpStatusCode.Ok,
data: {
defaultProtocol,
wantedPrice,
options
}
}
}, true);
},
async estimate(body) {
const kind = body.wantedPrice !== undefined ? 'limitQuote' : 'quote';
const response = await request({
method: 'post',
url: `${baseUrl}${apiPaths[kind]}`,
headers,
data: transformers[kind].request(body)
});
if (isLeft(response)) return parseMuesliError(response);
try {
return freeze({
tag: 'right',
value: {
status: response.value.status,
data: transformers.quote.response(response.value.data)
}
}, true);
} catch (e) {
return freeze({
tag: 'left',
error: {
status: -3,
message: 'No liquidity pools satisfy the estimate requirements',
responseData: response.value.data
}
}, true);
}
},
async create(body) {
const kind = body.wantedPrice !== undefined ? 'createLimit' : 'create';
const response = await request({
method: 'post',
url: `${baseUrl}${apiPaths[kind]}`,
headers,
data: transformers[kind].request(body)
});
if (isLeft(response)) return parseMuesliError(response);
return freeze({
tag: 'right',
value: {
status: response.value.status,
data: transformers[kind].response(response.value.data)
}
}, true);
},
async cancel(body) {
const response = await request({
method: 'post',
url: `${baseUrl}${apiPaths.cancel}`,
headers,
data: transformers.cancel.request(body)
});
if (isLeft(response)) return parseMuesliError(response);
return freeze({
tag: 'right',
value: {
status: response.value.status,
data: transformers.cancel.response(response.value.data)
}
}, true);
}
}, true);
};
export const parseMuesliError = ({
tag,
error
}) => freeze({
tag,
error: {
...error,
message: JSON.stringify(error.responseData?.detail ?? 'Muesliswap API error', null, 2).replace(/^"/, '').replace(/"$/, '')
}
}, true);
const baseUrls = freeze({
[Chain.Network.Mainnet]: 'https://aggregator-v2.muesliswap.com'
});
const apiPaths = freeze({
tokens: '/tokens',
orderHistory: '/order_history',
openOrders: '/open_orders',
quote: '/quote',
limitQuote: '/limit_order_quote',
create: '/order',
createLimit: '/limit_order',
cancel: '/cancel',
pools: '/pools',
providers: '/providers'
});
//# sourceMappingURL=api-maker.js.map