edge-core-js
Version:
Edge account & wallet management library
259 lines (228 loc) • 8.47 kB
JavaScript
function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }import { gt, lt } from 'biggystring'
import { bridgifyObject, close } from 'yaob'
import { upgradeSwapQuote } from '../../types/type-helpers'
import {
asMaybeInsufficientFundsError,
asMaybePendingFundsError,
asMaybeSwapAboveLimitError,
asMaybeSwapBelowLimitError,
asMaybeSwapCurrencyError,
asMaybeSwapPermissionError,
} from '../../types/types'
import { fuzzyTimeout, timeout } from '../../util/promise'
/**
* Fetch quotes from all plugins, and sorts the best ones to the front.
*/
export async function fetchSwapQuotes(
ai,
accountId,
request,
opts = {}
) {
const {
disabled = {},
noResponseMs,
preferPluginId,
promoCodes = {},
slowResponseMs = 20000
} = opts
const { log, state } = ai.props
const account = state.accounts[accountId]
const { swapSettings, userSettings } = account
const swapPlugins = state.plugins.swap
log.warn(
'Requesting swap quotes for: ',
{
...request,
fromWallet: request.fromWallet.id,
toWallet: request.toWallet.id
},
{ preferPluginId, promoCodes }
)
// Invoke all the active swap plugins:
const pendingIds = new Set()
const promises = []
for (const pluginId of Object.keys(swapPlugins)) {
const { enabled = true } =
swapSettings[pluginId] != null ? swapSettings[pluginId] : {}
if (!enabled || disabled[pluginId]) continue
// Start request:
pendingIds.add(pluginId)
promises.push(
swapPlugins[pluginId]
.fetchSwapQuote(request, userSettings[pluginId], {
infoPayload: _nullishCoalesce(_optionalChain([state, 'access', _ => _.infoCache, 'access', _2 => _2.corePlugins, 'optionalAccess', _3 => _3[pluginId]]), () => ( {})),
promoCode: promoCodes[pluginId]
})
.then(
quote => {
upgradeSwapQuote(quote)
const { fromWallet, toWallet, ...request } = _nullishCoalesce(quote.request, () => ( {}))
const cleaned = { ...quote, request }
pendingIds.delete(pluginId)
log.warn(`${pluginId} gave swap quote:`, cleaned)
return quote
},
error => {
pendingIds.delete(pluginId)
log.warn(`${pluginId} gave swap error: ${String(error)}`)
// Log unknown errors:
if (isUnknownSwapError(error)) {
log.crash(`Unknown swap error: ${String(error)}`, {
error,
swapPluginId: pluginId,
request: {
// Stringify to include "null"
fromToken: String(request.fromTokenId),
fromWalletType: request.fromWallet.type,
// Stringify to include "null"
toToken: String(request.toTokenId),
toWalletType: request.toWallet.type,
quoteFor: request.quoteFor
}
})
}
throw error
}
)
)
}
// Wait for the results, with error handling:
const promise = fuzzyTimeout(promises, slowResponseMs).then(
({ results: quotes, errors }) => {
for (const pluginId of pendingIds) {
log.warn(`${pluginId} gave swap timeout`)
}
// Find the cheapest price:
const sorted = sortQuotes(quotes, opts)
log.warn(
`${promises.length} swap quotes requested, ${quotes.length} resolved, ${
errors.length
} failed, sorted ${sorted.map(quote => quote.pluginId).join(', ')}.`
)
// Prepare quotes for the bridge:
return quotes.map(quote => wrapQuote(swapPlugins, request, quote))
},
(errors) => {
log.warn(`All ${promises.length} swap quotes rejected.`)
throw pickBestError(errors)
}
)
if (noResponseMs == null) return await promise
return await timeout(promise, noResponseMs)
}
function wrapQuote(
swapPlugins,
request,
quote
) {
const out = bridgifyObject({
canBePartial: quote.canBePartial,
expirationDate: quote.expirationDate,
fromNativeAmount: quote.fromNativeAmount,
isEstimate: quote.isEstimate,
maxFulfillmentSeconds: quote.maxFulfillmentSeconds,
networkFee: quote.networkFee,
pluginId: quote.pluginId,
request: _nullishCoalesce(quote.request, () => ( request)),
swapInfo: _nullishCoalesce(quote.swapInfo, () => ( swapPlugins[quote.pluginId].swapInfo)),
toNativeAmount: quote.toNativeAmount,
minReceiveAmount: quote.minReceiveAmount,
async approve(opts) {
return await quote.approve(opts)
},
async close() {
await quote.close()
close(out)
}
})
return out
}
/**
* Sorts the best quotes first.
*/
export function sortQuotes(
quotes,
opts
) {
const { preferPluginId, preferType, promoCodes } = opts
return quotes.sort((a, b) => {
// Prioritize transfer plugin:
if (a.pluginId === 'transfer') return -1
if (b.pluginId === 'transfer') return 1
// Always return quotes from the preferred provider:
if (a.pluginId === preferPluginId) return -1
if (b.pluginId === preferPluginId) return 1
// Prefer based on plugin but always allow `transfer` plugins:
if (preferType != null) {
const aMatchesType =
(a.swapInfo.isDex === true && preferType === 'DEX') ||
(a.swapInfo.isDex !== true && preferType === 'CEX')
const bMatchesType =
(b.swapInfo.isDex === true && preferType === 'DEX') ||
(b.swapInfo.isDex !== true && preferType === 'CEX')
if (aMatchesType && !bMatchesType) return -1
if (!aMatchesType && bMatchesType) return 1
}
// Prioritize providers with active promo codes:
if (promoCodes != null) {
const aHasPromo = promoCodes[a.pluginId] != null
const bHasPromo = promoCodes[b.pluginId] != null
if (aHasPromo && !bHasPromo) return -1
if (!aHasPromo && bHasPromo) return 1
}
// Prioritize accurate quotes over estimates:
const { isEstimate: aIsEstimate = true } = a
const { isEstimate: bIsEstimate = true } = b
if (!aIsEstimate && bIsEstimate) return -1
if (aIsEstimate && !bIsEstimate) return 1
// Prefer the best rate:
const aRate = Number(a.toNativeAmount) / Number(a.fromNativeAmount)
const bRate = Number(b.toNativeAmount) / Number(b.fromNativeAmount)
if (aRate > bRate) return -1
if (bRate > aRate) return 1
return 0
})
}
/**
* Picks the best error out of the available choices.
*/
function pickBestError(errors) {
return errors.reduce((a, b) => {
// Return the highest-ranked error:
const diff = rankError(a) - rankError(b)
if (diff > 0) return a
if (diff < 0) return b
// Same ranking, so use amounts to distinguish:
const aBelow = asMaybeSwapBelowLimitError(a)
const bBelow = asMaybeSwapBelowLimitError(b)
if (aBelow != null && bBelow != null) {
return lt(aBelow.nativeMin, bBelow.nativeMin) ? aBelow : bBelow
}
const aAbove = asMaybeSwapAboveLimitError(a)
const bAbove = asMaybeSwapAboveLimitError(b)
if (aAbove != null && bAbove != null) {
return gt(aAbove.nativeMax, bAbove.nativeMax) ? aAbove : bAbove
}
// Otherwise, just pick one:
return a
})
}
/**
* Ranks different error codes by priority.
*/
function rankError(error) {
if (error == null) return 0
if (asMaybeInsufficientFundsError(error) != null) return 6
if (asMaybePendingFundsError(error) != null) return 6
if (asMaybeSwapBelowLimitError(error) != null) return 5
if (asMaybeSwapAboveLimitError(error) != null) return 4
if (asMaybeSwapPermissionError(error) != null) return 3
if (asMaybeSwapCurrencyError(error) != null) return 2
return 1
}
function isUnknownSwapError(error) {
const isKnownError = rankError(error) > 1
// NOTE: Add more error filtering here as we decide to filter out noise.
return !isKnownError
}