UNPKG

@yoroi/swap

Version:
239 lines (238 loc) 8.57 kB
"use strict"; import { Api, Swap } from '@yoroi/types'; import { isLeft, isRight } from '@yoroi/common'; import { freeze } from 'immer'; import { dexhunterApiMaker } from './adapters/api/dexhunter/api-maker'; import { muesliswapApiMaker } from './adapters/api/muesliswap/api-maker'; import { getBestSwap } from './helpers/getBestSwap'; import { getPtPrice } from './helpers/getPtPrice'; export const swapManagerMaker = ({ address, addressHex, network, primaryTokenInfo, isPrimaryToken, stakingKey, storage, partners }) => { const dexhunterApi = dexhunterApiMaker({ address, network, primaryTokenInfo, isPrimaryToken, partner: partners?.[Swap.Aggregator.Dexhunter] }); const muesliswapApi = muesliswapApiMaker({ address, addressHex, network, primaryTokenInfo, stakingKey, isPrimaryToken, partner: partners?.[Swap.Aggregator.Muesliswap] }); const settings = { routingPreference: 'auto', slippage: 1 }; const assignSettings = v => { const newSettings = Object.assign(settings, v); storage.settings.save(newSettings); return newSettings; }; storage.settings.read().then(assignSettings); const api = apiManagerMaker({ [Swap.Aggregator.Dexhunter]: dexhunterApi, [Swap.Aggregator.Muesliswap]: muesliswapApi }, settings, getPtPrice(primaryTokenInfo, dexhunterApi)); return { api, assignSettings, settings, clearStorage: storage.clear }; }; const apiManagerMaker = (adapters, settings, getPrice) => { return freeze({ async tokens() { const aggregatorPromises = { dexhunter: adapters.dexhunter.tokens(), muesliswap: adapters.muesliswap.tokens() }; const responses = await Promise.all(Object.entries(aggregatorPromises).map(([key, promise]) => settings.routingPreference === 'auto' || settings.routingPreference.includes(key) ? promise : excluded)); warnAllLeft(...responses); if (responses.every(isLeft)) return invalid; const merged = {}; const append = tokenInfo => { if (merged[tokenInfo.id] === undefined) merged[tokenInfo.id] = tokenInfo; }; responses.filter(isRight).flatMap(({ value }) => value.data).forEach(append); return { tag: 'right', value: { status: Api.HttpStatusCode.Ok, data: Object.values(merged) } }; }, async orders() { const responses = await Promise.all([adapters.muesliswap.orders(), adapters.dexhunter.orders()]); warnAllLeft(...responses); if (responses.every(isLeft)) return invalid; const merged = {}; const append = order => { const key = `${order.txHash}#${order.outputIndex}`; /* istanbul ignore next */ if ( // TODO: refactor to avoid istanbul ignore merged[key] === undefined || order.aggregator === Swap.Aggregator.Dexhunter) merged[key] = order; }; responses.filter(isRight).flatMap(({ value }) => value.data).forEach(append); return { tag: 'right', value: { status: Api.HttpStatusCode.Ok, data: Object.values(merged).sort(({ lastUpdate: A, placedAt: A2 }, { lastUpdate: B, placedAt: B2 }) => (B ?? B2 ?? 0) - (A ?? A2 ?? 0)) } }; }, /* istanbul ignore next */ async limitOptions(body) { const aggregatorPromises = { dexhunter: adapters.dexhunter.limitOptions(body), muesliswap: adapters.muesliswap.limitOptions(body) }; const responses = await Promise.all(Object.entries(aggregatorPromises).map(([key, promise]) => settings.routingPreference === 'auto' || settings.routingPreference.includes(key) ? promise : excluded)); warnAllLeft(...responses); if (responses.every(isLeft)) return responses.find(res => res.error.status !== -3) ?? invalid; const validResponses = responses.filter(isRight).map(({ value }) => value.data); if (validResponses.length === 0) return invalid; const mergedOptions = {}; const append = res => { mergedOptions[res.protocol] = res; }; validResponses.forEach(({ options }) => options.forEach(append)); const data = { defaultProtocol: validResponses[0].defaultProtocol, wantedPrice: Math.min(...validResponses.map(({ wantedPrice }) => wantedPrice)), options: Object.values(mergedOptions) }; return { tag: 'right', value: { status: Api.HttpStatusCode.Ok, data } }; }, async estimate(body) { const aggregatorPromises = { dexhunter: adapters.dexhunter.estimate(body), muesliswap: adapters.muesliswap.estimate(body) }; const responses = await Promise.all(Object.entries(aggregatorPromises).map(([key, promise]) => settings.routingPreference === 'auto' || settings.routingPreference.includes(key) ? promise : excluded)); warnAllLeft(...responses); if (responses.every(isLeft)) return standarizeError(responses.find(res => res.error.status !== -3 && res.error.message !== '' && !res.error.message.includes('DOCTYPE html')) ?? invalid); const estimates = responses.filter(isRight).flatMap(({ value }) => value.data); const bestEstimate = estimates.reduce(getBestSwap(await getPrice(body.tokenOut)), estimates[0]); return { tag: 'right', value: { status: Api.HttpStatusCode.Ok, data: bestEstimate } }; }, async create(body) { const aggregatorPromises = { dexhunter: adapters.dexhunter.create(body), muesliswap: adapters.muesliswap.create(body) }; const responses = await Promise.all(Object.entries(aggregatorPromises).map(([key, promise]) => settings.routingPreference === 'auto' || settings.routingPreference.includes(key) ? promise : excluded)); warnAllLeft(...responses); if (responses.every(isLeft)) return standarizeError(responses.find(res => res.error.status !== -3 && res.error.message !== '' && !res.error.message.includes('DOCTYPE html')) ?? invalid); const creates = responses.filter(isRight).map(({ value }) => value.data); const bestCreate = creates.reduce(getBestSwap(await getPrice(body.tokenOut)), creates[0]); return { tag: 'right', value: { status: Api.HttpStatusCode.Ok, data: bestCreate } }; }, async cancel(body) { return body.order.aggregator === Swap.Aggregator.Muesliswap ? adapters.muesliswap.cancel(body) : adapters.dexhunter.cancel(body); } }, true); }; const excluded = freeze({ tag: 'left', error: { status: -3, message: 'Aggregator excluded from call', responseData: {} } }, true); const invalid = freeze({ tag: 'left', error: { status: -3, message: 'Unknown error', responseData: {} } }, true); const warnAllLeft = (...responses) => { if (responses.every(isLeft)) console.warn('Swap Manager all left >> ', responses.map(response => response.error.message)); }; export const standarizeError = input => { if (isRight(input)) return input; const response = { ...input, error: { ...input.error } }; switch (true) { case response.error.message.includes('Unable to build transaction due to insufficient user balance'): case response.error.message.includes('Transaction Building Errornot enough funds'): case response.error.message.includes('Transaction Building ErrorNo Remaining UTxOs'): response.error.message = 'Insufficient balance: consider fees, assets blocked by staking or multiaddress holdings'; break; case response.error.message.includes('amount_in_invalid'): case response.error.message.includes('Buy and sell amounts must be positive'): response.error.message = 'Buy and sell amounts must be positive'; break; case response.error.message.includes('No liquidity available for this token pair'): case response.error.message.includes('pool_not_found'): response.error.message = 'This pair is not available in any liquidity pool.'; break; case response.error.message.includes('DOCTYPE html'): case response.error.message.includes('Could not find the number of decimals'): response.error.message = 'Unknown error'; break; } return freeze(response, true); }; //# sourceMappingURL=manager.js.map