Skip to content

Commit

Permalink
Merge pull request #570 from EdgeApp/william/fetch-swap-quotes
Browse files Browse the repository at this point in the history
Fetch swap quotes
  • Loading branch information
swansontec authored Oct 18, 2023
2 parents 3b2f7e1 + 3e4c3ea commit 3f5270c
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 48 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# edge-core-js

## Unreleased

- added: `EdgeAccount.fetchSwapQuotes`, to return all relevant quotes, and not just the best one.

## 1.10.0 (2023-10-10)

- added: EdgeTxAction types to tag known smart contract trasaction types (swap, stake, etc)
- added: `EdgeTransaction.action` to tag known smart contract transaction types (swap, stake, etc.).

## 1.9.0 (2023-10-10)

Expand Down
25 changes: 23 additions & 2 deletions src/core/account/account-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ import {
import { ApiInput } from '../root-pixie'
import { makeLocalDisklet } from '../storage/repo'
import { makeStorageWalletApi } from '../storage/storage-api'
import { fetchSwapQuote } from '../swap/swap-api'
import { fetchSwapQuotes } from '../swap/swap-api'
import { changeWalletStates } from './account-files'
import { AccountState } from './account-reducer'
import { makeDataStoreApi } from './data-store-api'
Expand Down Expand Up @@ -625,7 +625,28 @@ export function makeAccountApi(ai: ApiInput, accountId: string): EdgeAccount {
request: EdgeSwapRequest,
opts?: EdgeSwapRequestOptions
): Promise<EdgeSwapQuote> {
return await fetchSwapQuote(ai, accountId, request, opts)
const [bestQuote, ...otherQuotes] = await fetchSwapQuotes(
ai,
accountId,
request,
opts
)

// Close unused quotes:
for (const otherQuote of otherQuotes) {
otherQuote.close().catch(() => undefined)
}

// Return the front quote:
if (bestQuote == null) throw new Error('No swap providers enabled')
return bestQuote
},

async fetchSwapQuotes(
request: EdgeSwapRequest,
opts?: EdgeSwapRequestOptions
): Promise<EdgeSwapQuote[]> {
return await fetchSwapQuotes(ai, accountId, request, opts)
}
}
bridgifyObject(out)
Expand Down
92 changes: 58 additions & 34 deletions src/core/swap/swap-api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { gt, lt } from 'biggystring'
import { bridgifyObject } from 'yaob'
import { bridgifyObject, close } from 'yaob'

import { upgradeCurrencyCode } from '../../types/type-helpers'
import {
Expand All @@ -9,6 +9,8 @@ import {
asMaybeSwapBelowLimitError,
asMaybeSwapCurrencyError,
asMaybeSwapPermissionError,
EdgePluginMap,
EdgeSwapPlugin,
EdgeSwapQuote,
EdgeSwapRequest,
EdgeSwapRequestOptions
Expand All @@ -17,14 +19,14 @@ import { fuzzyTimeout } from '../../util/promise'
import { ApiInput } from '../root-pixie'

/**
* Fetch quotes from all plugins, and pick the best one.
* Fetch quotes from all plugins, and sorts the best ones to the front.
*/
export async function fetchSwapQuote(
export async function fetchSwapQuotes(
ai: ApiInput,
accountId: string,
request: EdgeSwapRequest,
opts: EdgeSwapRequestOptions = {}
): Promise<EdgeSwapQuote> {
): Promise<EdgeSwapQuote[]> {
const { disabled = {}, preferPluginId, promoCodes = {} } = opts
const { log } = ai.props

Expand Down Expand Up @@ -94,7 +96,6 @@ export async function fetchSwapQuote(
)
)
}
if (promises.length < 1) throw new Error('No swap providers enabled')

// Wait for the results, with error handling:
return fuzzyTimeout(promises, 20000).then(
Expand All @@ -104,21 +105,15 @@ export async function fetchSwapQuote(
}

// Find the cheapest price:
const bestQuote = pickBestQuote(quotes, opts)
const sorted = sortQuotes(quotes, opts)
log.warn(
`${promises.length} swap quotes requested, ${quotes.length} resolved, ${errors.length} failed, picked ${bestQuote.pluginId}.`
`${promises.length} swap quotes requested, ${quotes.length} resolved, ${
errors.length
} failed, sorted ${sorted.map(quote => quote.pluginId).join(', ')}.`
)

// Close unused quotes:
for (const quote of quotes) {
if (quote !== bestQuote) quote.close().catch(() => undefined)
}
// @ts-expect-error - Here for backwards compatibility:
bestQuote.request = request
// @ts-expect-error - Here for backwards compatibility:
bestQuote.swapInfo = swapPlugins[bestQuote.pluginId].swapInfo

return bridgifyObject(bestQuote)
// Prepare quotes for the bridge:
return quotes.map(quote => wrapQuote(swapPlugins, request, quote))
},
(errors: unknown[]) => {
log.warn(`All ${promises.length} swap quotes rejected.`)
Expand All @@ -127,24 +122,51 @@ export async function fetchSwapQuote(
)
}

function wrapQuote(
swapPlugins: EdgePluginMap<EdgeSwapPlugin>,
request: EdgeSwapRequest,
quote: EdgeSwapQuote
): EdgeSwapQuote {
const out = bridgifyObject<EdgeSwapQuote>({
canBePartial: quote.canBePartial,
expirationDate: quote.expirationDate,
fromNativeAmount: quote.fromNativeAmount,
isEstimate: quote.isEstimate,
maxFulfillmentSeconds: quote.maxFulfillmentSeconds,
networkFee: quote.networkFee,
pluginId: quote.pluginId,
request: quote.request ?? request,
swapInfo: quote.swapInfo ?? swapPlugins[quote.pluginId].swapInfo,
toNativeAmount: quote.toNativeAmount,

async approve(opts) {
return await quote.approve(opts)
},

async close() {
await quote.close()
close(out)
}
})
return out
}

/**
* Picks the best quote out of the available choices.
* Exported so we can unit-test it.
* Sorts the best quotes first.
*/

export function pickBestQuote(
export function sortQuotes(
quotes: EdgeSwapQuote[],
opts: EdgeSwapRequestOptions
): EdgeSwapQuote {
): EdgeSwapQuote[] {
const { preferPluginId, preferType, promoCodes } = opts
return quotes.reduce((a, b) => {
return quotes.sort((a, b) => {
// Prioritize transfer plugin:
if (a.pluginId === 'transfer') return a
if (b.pluginId === 'transfer') return b
if (a.pluginId === 'transfer') return -1
if (b.pluginId === 'transfer') return 1

// Always return quotes from the preferred provider:
if (a.pluginId === preferPluginId) return a
if (b.pluginId === preferPluginId) return b
if (a.pluginId === preferPluginId) return -1
if (b.pluginId === preferPluginId) return 1

// Prefer based on plugin but always allow `transfer` plugins:
if (preferType != null) {
Expand All @@ -154,28 +176,30 @@ export function pickBestQuote(
const bMatchesType =
(b.swapInfo.isDex === true && preferType === 'DEX') ||
(b.swapInfo.isDex !== true && preferType === 'CEX')
if (aMatchesType && !bMatchesType) return a
if (!aMatchesType && bMatchesType) return b
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 a
if (!aHasPromo && bHasPromo) return b
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 b
if (!aIsEstimate && bIsEstimate) return a
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)
return bRate > aRate ? b : a
if (aRate > bRate) return -1
if (bRate > aRate) return 1
return 0
})
}

Expand Down
4 changes: 4 additions & 0 deletions src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1587,6 +1587,10 @@ export interface EdgeAccount {
request: EdgeSwapRequest,
opts?: EdgeSwapRequestOptions
) => Promise<EdgeSwapQuote>
readonly fetchSwapQuotes: (
request: EdgeSwapRequest,
opts?: EdgeSwapRequestOptions
) => Promise<EdgeSwapQuote[]>
}

// ---------------------------------------------------------------------
Expand Down
25 changes: 14 additions & 11 deletions test/core/swap.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect } from 'chai'
import { describe, it } from 'mocha'

import { pickBestQuote } from '../../src/core/swap/swap-api'
import { sortQuotes } from '../../src/core/swap/swap-api'
import { EdgeSwapInfo, EdgeSwapQuote, EdgeSwapRequest } from '../../src/index'

const typeHack: any = {}
Expand Down Expand Up @@ -77,32 +77,35 @@ const quotes: EdgeSwapQuote[] = [
]

describe('swap', function () {
const getIds = (quotes: EdgeSwapQuote[]): string =>
quotes.map(quote => quote.pluginId).join(', ')

it('picks the best quote', function () {
const quote = pickBestQuote(quotes, {})
expect(quote.pluginId).equals('changenow')
const sorted = sortQuotes(quotes, {})
expect(getIds(sorted)).equals('changenow, godex, thorchain, switchain')
})

it('picks the preferred swap provider', function () {
const quote = pickBestQuote(quotes, { preferPluginId: 'switchain' })
expect(quote.pluginId).equals('switchain')
const sorted = sortQuotes(quotes, { preferPluginId: 'switchain' })
expect(getIds(sorted)).equals('switchain, changenow, godex, thorchain')
})

it('picks the preferred swap type DEX', function () {
const quote = pickBestQuote(quotes, { preferType: 'DEX' })
expect(quote.pluginId).equals('thorchain')
const sorted = sortQuotes(quotes, { preferType: 'DEX' })
expect(getIds(sorted)).equals('thorchain, changenow, godex, switchain')
})

it('picks the preferred swap type CEX', function () {
const quote = pickBestQuote(quotes, { preferType: 'CEX' })
expect(quote.pluginId).equals('changenow')
const sorted = sortQuotes(quotes, { preferType: 'CEX' })
expect(getIds(sorted)).equals('changenow, godex, switchain, thorchain')
})

it('picks the swap provider with an active promo code', function () {
const quote = pickBestQuote(quotes, {
const sorted = sortQuotes(quotes, {
promoCodes: {
switchain: 'deal10'
}
})
expect(quote.pluginId).equals('switchain')
expect(getIds(sorted)).equals('switchain, changenow, godex, thorchain')
})
})

0 comments on commit 3f5270c

Please sign in to comment.