UNPKG

@yoroi/swap

Version:
324 lines (315 loc) 12.1 kB
"use strict"; import { isLeft, isRight } from '@yoroi/common'; import { Api, Swap } from '@yoroi/types'; import { freeze } from 'immer'; import { dexhunterApiMaker } from './adapters/api/dexhunter/api-maker'; import { minswapApiMaker } from './adapters/api/minswap/api-maker'; import { muesliswapApiMaker } from './adapters/api/muesliswap/api-maker'; import { steelswapApiMaker } from './adapters/api/steelswap/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 minswapApi = minswapApiMaker({ address, network, primaryTokenInfo, isPrimaryToken, partner: partners?.[Swap.Aggregator.Minswap] }); const steelswapApi = steelswapApiMaker({ address, network, primaryTokenInfo, isPrimaryToken, partner: partners?.[Swap.Aggregator.Steelswap] }); 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); // Only include adapters that have a partner code in the partners object const adapters = {}; if (partners?.[Swap.Aggregator.Dexhunter]) { adapters[Swap.Aggregator.Dexhunter] = dexhunterApi; } if (partners?.[Swap.Aggregator.Muesliswap]) { adapters[Swap.Aggregator.Muesliswap] = muesliswapApi; } if (partners?.[Swap.Aggregator.Minswap]) { adapters[Swap.Aggregator.Minswap] = minswapApi; } if (partners?.[Swap.Aggregator.Steelswap]) { adapters[Swap.Aggregator.Steelswap] = steelswapApi; } const api = apiManagerMaker(adapters, settings, getPtPrice(primaryTokenInfo, dexhunterApi)); return { api, assignSettings, settings, clearStorage: storage.clear }; }; const apiManagerMaker = (adapters, settings, getPrice) => { // Helper function to get enabled aggregators based on routing preference const getEnabledAggregators = () => { if (settings.routingPreference === 'auto') { return Object.keys(adapters); } // Filter to only include aggregators that exist in adapters return settings.routingPreference.filter(agg => adapters[agg] !== undefined); }; return freeze({ async tokens() { const enabledAggregators = getEnabledAggregators(); const settledResults = await Promise.allSettled(enabledAggregators.map(aggregator => adapters[aggregator].tokens())); const responses = []; const errors = []; settledResults.forEach((result, index) => { if (result.status === 'fulfilled') { responses.push(result.value); } else { errors.push(`Aggregator ${enabledAggregators[index]} failed: ${result.reason}`); } }); if (errors.length > 0) { console.warn('Some aggregators failed:', errors); } 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 enabledAggregators = Object.keys(adapters).filter(agg => adapters[agg] !== undefined); const responses = await Promise.all(enabledAggregators.map(aggregator => adapters[aggregator].orders())); warnAllLeft(...responses); if (responses.every(isLeft)) return invalid; const merged = {}; const append = order => { const key = `${order.txHash}#${order.outputIndex}`; if (merged[key] === undefined || order.aggregator === Swap.Aggregator.Dexhunter) merged[key] = order; // Make sure we have Dexhunter's customId in case we need to cancel the order with them if (order.customId && merged[key] && merged[key].customId === undefined) merged[key] = { ...merged[key], customId: order.customId }; }; 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)) } }; }, async limitOptions(body) { const enabledAggregators = getEnabledAggregators(); const responses = await Promise.all(enabledAggregators.map(aggregator => adapters[aggregator].limitOptions(body))); 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).filter(option => option.protocol !== Swap.Protocol.Unsupported) }; return { tag: 'right', value: { status: Api.HttpStatusCode.Ok, data } }; }, async estimate(body) { const enabledAggregators = getEnabledAggregators(); const settledResults = await Promise.allSettled(enabledAggregators.map(async aggregator => { // If amountOut is provided and adapter supports reverse estimate, rely on adapter implementation. const response = await adapters[aggregator].estimate(body); return response; })); const responses = []; const errors = []; settledResults.forEach((result, index) => { if (result.status === 'fulfilled') { responses.push(result.value); } else { errors.push(`Aggregator ${enabledAggregators[index]} failed: ${result.reason}`); } }); if (errors.length > 0) { console.warn('Some aggregators failed during estimate:', errors); } 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); if (estimates.length === 0) { return invalid; } const bestEstimate = estimates.reduce(getBestSwap(await getPrice(body.tokenOut)), estimates[0]); return { tag: 'right', value: { status: Api.HttpStatusCode.Ok, data: bestEstimate } }; }, async create(body) { // Feature flag: single adapter create (default true) const singleAdapterCreate = true; if (singleAdapterCreate && body.routeHint?.aggregator != null) { const adapter = adapters[body.routeHint.aggregator]; if (adapter == null) return invalid; const response = await adapter.create(body); if (isLeft(response)) return standarizeError(response); return response; } // Fallback to legacy fan-out const enabledAggregators = getEnabledAggregators(); const responses = await Promise.all(enabledAggregators.map(aggregator => adapters[aggregator].create(body))); 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) { // Helper function to check if response has valid CBOR const hasValidCbor = response => { return isRight(response) && response.value.data.cbor.trim() !== ''; }; // First, try the appropriate adapter based on aggregator const initialAdapter = body.order.aggregator === Swap.Aggregator.Muesliswap ? adapters[Swap.Aggregator.Muesliswap] : body.order.aggregator === Swap.Aggregator.Minswap ? adapters[Swap.Aggregator.Minswap] : body.order.aggregator === Swap.Aggregator.Steelswap ? adapters[Swap.Aggregator.Steelswap] : adapters[Swap.Aggregator.Dexhunter]; if (!initialAdapter) return invalid; const initialResponse = await initialAdapter.cancel(body); // If we got a valid CBOR, return it if (hasValidCbor(initialResponse)) { return initialResponse; } // If not, try all other adapters in parallel const otherAggregators = Object.entries(adapters).filter(([_, adapter]) => adapter !== initialAdapter); const alternativeResponses = await Promise.all(otherAggregators.map(([_, adapter]) => adapter.cancel(body))); // Find the first response with valid CBOR const validResponse = alternativeResponses.find(hasValidCbor); // If found, return it; otherwise return the initial response return validResponse ?? initialResponse; } }, 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'): case response.error.message.includes('Insufficient balance'): response.error.message = 'Insufficient balance: consider fees and assets blocked by staking.'; 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