Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(backend): add local payment #2857

Merged
merged 81 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
bb77152
feat(backend): add local payment quote migration
BlairCurrey Aug 10, 2024
fc06e24
feat(backend): WIP seperate ILPModels, LocalQuote, BaseQuote models
BlairCurrey Aug 12, 2024
257fd01
refactor(backend): change model/services to reflect optional ilp quot…
BlairCurrey Aug 13, 2024
d9fa20e
Merge branch 'main' into bc/2834/non-ilp-local-payments
BlairCurrey Aug 14, 2024
2e76ee2
feat(backend): WIP local payment method with getQuote
BlairCurrey Aug 15, 2024
4fab5d8
feat(backend): add local payment method to payment method handler
BlairCurrey Aug 15, 2024
b17db75
chore(backend): fix format
BlairCurrey Aug 15, 2024
6e37420
Merge branch 'main' into bc/2834/non-ilp-local-payments
BlairCurrey Aug 15, 2024
9ef7f94
feat(backend): stub in control payment handler service with receiver …
BlairCurrey Aug 16, 2024
320ee21
feat(backend): local payment .pay
BlairCurrey Aug 20, 2024
07569a7
Merge branch 'main' into bc/2834/non-ilp-local-payments
BlairCurrey Aug 20, 2024
f3d5f5d
chore: rm comment
BlairCurrey Aug 20, 2024
d795ce2
chore: WIP debugging wrong sentAmount
BlairCurrey Aug 21, 2024
251c60a
chore: rm comment
BlairCurrey Aug 21, 2024
8f0bcb8
Merge branch 'main' into bc/2834/non-ilp-local-payments
BlairCurrey Aug 21, 2024
eca01f1
feat(backend): use receiver.isLocal to control payment method in quot…
BlairCurrey Aug 21, 2024
770d7ea
Merge branch 'main' into bc/2834/non-ilp-local-payments
BlairCurrey Aug 23, 2024
3849db7
Merge branch 'main' into bc/2834/non-ilp-local-payments
BlairCurrey Sep 10, 2024
170bcc3
fix(backend): added source amount
BlairCurrey Sep 13, 2024
88618ce
fix(backend): p2p case (cross currency, local, fixed send)
BlairCurrey Sep 16, 2024
823c512
fix: lint error
BlairCurrey Sep 16, 2024
2e9f043
chore: rm logs
BlairCurrey Sep 16, 2024
3fb02e5
fix: quote service test
BlairCurrey Sep 16, 2024
569f029
fix: lint errors
BlairCurrey Sep 16, 2024
dae1de1
Merge branch 'main' into bc/2834/non-ilp-local-payments
BlairCurrey Sep 16, 2024
2016469
Merge branch 'main' into bc/2834/non-ilp-local-payments
BlairCurrey Sep 18, 2024
3182944
refactor(backend): split migrations
BlairCurrey Sep 18, 2024
3665188
refactor(backend): rm migration that was split into many
BlairCurrey Sep 18, 2024
5b8bad3
WIP bruno requests for testing
BlairCurrey Sep 18, 2024
b49e08e
feat(backend): start rm ilpQuoteDetail join on op where not used
BlairCurrey Sep 18, 2024
eedacb5
fix(backend): rm unecessary ilpQuoteDetail join
BlairCurrey Sep 18, 2024
27a64b2
chore(backend): format
BlairCurrey Sep 18, 2024
26d0539
fix(backend): dont join op on quote.ilpQuoteDetails on get
BlairCurrey Sep 18, 2024
2723961
fix(backend): rm ilpQuoteDetails join on op cancel
BlairCurrey Sep 18, 2024
08126c5
fix(backend): rm unecessary join in op validate grant amount
BlairCurrey Sep 18, 2024
205447d
fix(backend): rm join from fundPayment
BlairCurrey Sep 18, 2024
0471d84
fix(backend): rm unecessary join, unused method
BlairCurrey Sep 18, 2024
7b5142d
fix(backend): fetch ilpQuoteDetails where used instead of joining
BlairCurrey Sep 18, 2024
773e4f2
chore(backend): move ilpquotedetails dir
BlairCurrey Sep 18, 2024
0a3adf4
chore(backend): rm console.log
BlairCurrey Sep 18, 2024
347f9b0
fix(backend): rm ilpQuoteDetails joins from quote service
BlairCurrey Sep 19, 2024
37d399e
chore(backend): rm console.log
BlairCurrey Sep 19, 2024
78d42b6
refactor(backend): rename sourceAmount to debitAmountMinusFees
BlairCurrey Sep 26, 2024
a96d654
chore(backend): cleanup, rm unused fee method
BlairCurrey Sep 30, 2024
7941f05
test(backend): add local payment tests
BlairCurrey Oct 1, 2024
753a57b
chore: format
BlairCurrey Oct 1, 2024
ed30066
Merge branch 'main' into bc/2834/non-ilp-local-payments
BlairCurrey Oct 1, 2024
8476615
fix(bruno): local open payments requests
BlairCurrey Oct 2, 2024
f8a979c
test(backend): add integration tests for local payments
BlairCurrey Oct 2, 2024
5cee618
chore(backend): cleanup
BlairCurrey Oct 2, 2024
d41777e
chore: restore old version of date definition in test
BlairCurrey Oct 2, 2024
e0f988d
chore: cleanup
BlairCurrey Oct 2, 2024
5d631bd
fix: rm unused import
BlairCurrey Oct 2, 2024
ececad2
test(integration): new case - p2p, fixed-send, local
BlairCurrey Oct 3, 2024
63011ae
chore(integration): rename test for consistency
BlairCurrey Oct 3, 2024
bd12e30
fix(backend): throw error in pay if incoming payment is not pending
BlairCurrey Oct 16, 2024
b0eaf8d
Merge branch 'main' into bc/2834/non-ilp-local-payments
BlairCurrey Oct 22, 2024
0d69a3d
feat(backend): simplify migrations
BlairCurrey Oct 23, 2024
31e6cc7
chore(backend): clarify comment
BlairCurrey Oct 23, 2024
bb9d334
chore(auth): format
BlairCurrey Oct 23, 2024
b0897d9
refactor(backend): use IlpQuoteDetails model directly in ilp payment …
BlairCurrey Oct 23, 2024
2d8048a
refactor(backend): rm ilpQuoteDetails service
BlairCurrey Oct 23, 2024
15d8929
Merge branch 'main' into bc/2834/non-ilp-local-payments
BlairCurrey Oct 29, 2024
c40db9e
fix(integration): wa typo
BlairCurrey Oct 30, 2024
0dbb2d3
chore: rm bruno test examples
BlairCurrey Oct 30, 2024
61b1010
refactor: mv debitAmountMinusFees to fee calc and clarify TODO
BlairCurrey Oct 30, 2024
0dea97d
Merge branch 'main' into bc/2834/non-ilp-local-payments
BlairCurrey Oct 30, 2024
6c995d9
Update bruno/collections/Rafiki/Examples/Admin API - only locally/Pe…
BlairCurrey Oct 31, 2024
86f6db6
fix: make timeout required again
BlairCurrey Oct 31, 2024
3fc6924
feat: error when post fails in local pay
BlairCurrey Oct 31, 2024
6634b8f
refactor(backend): add optional quoteId to getQuote args
BlairCurrey Nov 1, 2024
6706557
refactor: rm ilp quote details out of quote service
BlairCurrey Nov 1, 2024
998ee25
refactor: insert ilp quote details in ilp getQuote
BlairCurrey Nov 1, 2024
a4cf57a
fix(backend): payment handler test
BlairCurrey Nov 1, 2024
1835bc4
chore(bruno): rename request
BlairCurrey Nov 5, 2024
7db833b
chore(integration): rm erroneous todo comment
BlairCurrey Nov 5, 2024
4437438
Merge branch 'main' into bc/2834/non-ilp-local-payments
BlairCurrey Nov 5, 2024
3149428
Merge branch 'main' into bc/2834/non-ilp-local-payments
BlairCurrey Nov 8, 2024
fe63991
fix(backend): local quote amounts, estimatedExchangeRaet
BlairCurrey Nov 14, 2024
e6cad1e
chore(backend): format
BlairCurrey Nov 14, 2024
f120aab
refactor(backend): rate convert methods to be explicit
BlairCurrey Nov 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ export const RafikiServicesFactory = Factory.define<MockRafikiServices>(
await accounting._getByIncomingToken(token)
}))
.attr('rates', {
convert: async (opts) => opts.sourceAmount,
convert: async (opts) => ({
amount: opts.sourceAmount,
scaledExchangeRate: 1
}),
rates: () => {
throw new Error('unimplemented')
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from '../../../../../accounting/errors'
import { Transaction, TransferType } from '../../../../../accounting/service'
import { Config as AppConfig } from '../../../../../config/app'
import { isConvertError } from '../../../../../rates/service'
const { CannotReceiveError, InsufficientLiquidityError } = Errors

export function createBalanceMiddleware(): ILPMiddleware {
Expand Down Expand Up @@ -40,8 +41,7 @@ export function createBalanceMiddleware(): ILPMiddleware {
sourceAsset: accounts.incoming.asset,
destinationAsset: accounts.outgoing.asset
})
if (typeof destinationAmountOrError !== 'bigint') {
// ConvertError
if (isConvertError(destinationAmountOrError)) {
logger.error(
{
amount,
Expand All @@ -55,8 +55,9 @@ export function createBalanceMiddleware(): ILPMiddleware {
`Exchange rate error: ${destinationAmountOrError}`
)
}
const { amount: destinationAmount } = destinationAmountOrError

request.prepare.amount = destinationAmountOrError.toString()
request.prepare.amount = destinationAmount.toString()

if (state.unfulfillable) {
await next()
Expand All @@ -71,7 +72,7 @@ export function createBalanceMiddleware(): ILPMiddleware {
sourceAccount: accounts.incoming,
destinationAccount: accounts.outgoing,
sourceAmount,
destinationAmount: destinationAmountOrError,
destinationAmount,
transferType: TransferType.TRANSFER,
timeout: AppConfig.tigerBeetleTwoPhaseTimeout
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@ describe('Balance Middleware', function () {
const destinationAmount = BigInt(200)
jest
.spyOn(rates, 'convert')
.mockImplementationOnce(async () => destinationAmount)
.mockImplementationOnce(async () => ({
amount: destinationAmount,
scaledExchangeRate: 1
}))

await expect(middleware(ctx, next)).resolves.toBeUndefined()

Expand Down
8 changes: 5 additions & 3 deletions packages/backend/src/payment-method/local/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,9 @@ describe('LocalPaymentService', (): void => {
const ratesService = await deps.use('ratesService')
jest
.spyOn(ratesService, 'convert')
.mockImplementation(() => Promise.resolve(100n))
.mockImplementation(() =>
Promise.resolve({ amount: 100n, scaledExchangeRate: 1 })
)
expect.assertions(4)
try {
await localPaymentService.getQuote({
Expand Down Expand Up @@ -257,7 +259,7 @@ describe('LocalPaymentService', (): void => {
incomingAssetCode | incomingAmountValue | debitAssetCode | expectedDebitAmount | exchangeRate | description
${'USD'} | ${100n} | ${'USD'} | ${100n} | ${null} | ${'local currency'}
${'EUR'} | ${100n} | ${'USD'} | ${100n} | ${1.0} | ${'cross currency, same rate'}
${'EUR'} | ${100n} | ${'USD'} | ${111n} | ${0.9} | ${'cross currency, exchange rate < 1'}
${'EUR'} | ${100n} | ${'USD'} | ${112n} | ${0.9} | ${'cross currency, exchange rate < 1'}
${'EUR'} | ${100n} | ${'USD'} | ${50n} | ${2.0} | ${'cross currency, exchange rate > 1'}
`(
'$description',
Expand All @@ -272,7 +274,7 @@ describe('LocalPaymentService', (): void => {

if (incomingAssetCode !== debitAssetCode) {
ratesScope = mockRatesApi(exchangeRatesUrl, () => ({
[debitAssetCode]: 1 / exchangeRate
[incomingAssetCode]: exchangeRate
}))
}

Expand Down
62 changes: 35 additions & 27 deletions packages/backend/src/payment-method/local/service.ts
BlairCurrey marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
} from '../../accounting/errors'
import { IncomingPaymentService } from '../../open_payments/payment/incoming/service'
import { IncomingPaymentState } from '../../open_payments/payment/incoming/model'
import { ConvertResults } from '../../rates/util'

export interface LocalPaymentService extends PaymentMethodService {}

Expand Down Expand Up @@ -58,18 +59,21 @@ async function getQuote(
deps: ServiceDependencies,
options: StartQuoteOptions
): Promise<PaymentQuote> {
const { receiver, debitAmount, receiveAmount } = options
const { receiver, debitAmount, receiveAmount, walletAddress } = options

let debitAmountValue: bigint
let receiveAmountValue: bigint
let exchangeRate: number

const convert = async (opts: RateConvertOpts) => {
let converted: bigint | ConvertError
const convert = async (
opts: RateConvertOpts
): Promise<ConvertResults | ConvertError> => {
let convertResults: ConvertResults | ConvertError
try {
converted = await deps.ratesService.convert(opts)
convertResults = await deps.ratesService.convert(opts)
} catch (err) {
deps.logger.error(
{ receiver, debitAmount, receiveAmount, err },
{ opts, err },
'Unknown error while attempting to convert rates'
)
throw new PaymentMethodHandlerError(
Expand All @@ -80,23 +84,23 @@ async function getQuote(
}
)
}
return converted
return convertResults
}

if (debitAmount) {
debitAmountValue = debitAmount.value
const converted = await convert({
const convertResults = await convert({
sourceAmount: debitAmountValue,
sourceAsset: {
code: debitAmount.assetCode,
scale: debitAmount.assetScale
code: walletAddress.asset.code,
scale: walletAddress.asset.scale
},
destinationAsset: {
code: receiver.assetCode,
scale: receiver.assetScale
}
})
if (isConvertError(converted)) {
if (isConvertError(convertResults)) {
throw new PaymentMethodHandlerError(
'Received error during local quoting',
{
Expand All @@ -105,21 +109,23 @@ async function getQuote(
}
)
}
receiveAmountValue = converted
receiveAmountValue = convertResults.amount
exchangeRate = convertResults.scaledExchangeRate
} else if (receiveAmount) {
receiveAmountValue = receiveAmount.value
const converted = await convert({
const convertResults = await convert({
reverseDirection: true,
sourceAmount: receiveAmountValue,
Copy link
Contributor

@mkurapov mkurapov Nov 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about this: since we are moving money between source <> destination asset in the end (e.g. USD > EUR) we actually need sourceAmount & sourceAsset to always be the sending currency (USD),
since in reality it may cost more for a certain provider to move USD > EUR vs EUR > USD.

Basically, if we need to get the receiveAmount, instead of using EUR <> USD rate, we would do
converted = ceil(receiveAmount / USD <> EUR rate)

Copy link
Contributor Author

@BlairCurrey BlairCurrey Nov 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hardcoded in the rates because I'm not sure where to get the sending currency (USD), since there is no debitAmount here. Also, I'm not actually sure how to trigger this block as opposed to receiver.incomingAmount further down. That's what the fixed send p2p example triggers. Although I think your comment applies to both places so that detail probably doesnt matter.

Anyways, just wanted to validate this. With your calculation patched in and doing a fixed send local payment, I see an outgoing payment with these amounts:

      "receiveAmount": {
        "assetCode": "EUR",
        "assetScale": 2,
        "value": "1000"
      },
      "debitAmount": {
        "assetCode": "USD",
        "assetScale": 2,
        "value": "1221"
      },
      "sentAmount": {
        "assetCode": "USD",
        "assetScale": 2,
        "value": "1099"
      },

The way it is now, the receiveAmount is the same and debitAmount and sentAmount are 1 higher:

      "receiveAmount": {
        "assetCode": "EUR",
        "assetScale": 2,
        "value": "1000"
      },
      "debitAmount": {
        "assetCode": "USD",
        "assetScale": 2,
        "value": "1222"
      },
      "sentAmount": {
        "assetCode": "USD",
        "assetScale": 2,
        "value": "1100"
      },

For the same payment over ILP I see the same 1099 sentAmount from your revised calculation but a different debitAmount. Not sure if slippage would account for this (in which case there is no problem) or something else.

      "receiveAmount": {
        "assetCode": "EUR",
        "assetScale": 2,
        "value": "1000"
      },
      "debitAmount": {
        "assetCode": "USD",
        "assetScale": 2,
        "value": "1234"
      },
      "sentAmount": {
        "assetCode": "USD",
        "assetScale": 2,
        "value": "1099"
      },

Copy link
Contributor

@mkurapov mkurapov Nov 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure where to get the sending currency

We should have this on the walletAddress.asset. So for those two branches without a debitAmount, we do the calculation as described above:
debitAmountValue = ceil(receiveAmount / (scaled USD <> EUR rate))
(scaled because we could have a difference in assetScale)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, the estimated exchange rate can be the exchange rate that's received directly from the rates service, instead of having to do estimatedExchangeRate: Number(receiveAmountValue) / Number(debitAmountValue)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦 Of course, the destinationAsset passed in to the rates call.

sourceAsset: {
code: receiveAmount.assetCode,
scale: receiveAmount.assetScale
code: walletAddress.asset.code,
scale: walletAddress.asset.scale
},
destinationAsset: {
code: options.walletAddress.asset.code,
scale: options.walletAddress.asset.scale
code: receiveAmount.assetCode,
scale: receiveAmount.assetScale
}
})
if (isConvertError(converted)) {
if (isConvertError(convertResults)) {
throw new PaymentMethodHandlerError(
'Received error during local quoting',
{
Expand All @@ -128,21 +134,23 @@ async function getQuote(
}
)
}
debitAmountValue = converted
debitAmountValue = convertResults.amount
exchangeRate = convertResults.scaledExchangeRate
} else if (receiver.incomingAmount) {
receiveAmountValue = receiver.incomingAmount.value
const converted = await convert({
const convertResults = await convert({
reverseDirection: true,
sourceAmount: receiveAmountValue,
sourceAsset: {
code: receiver.incomingAmount.assetCode,
scale: receiver.incomingAmount.assetScale
code: walletAddress.asset.code,
scale: walletAddress.asset.scale
},
destinationAsset: {
code: options.walletAddress.asset.code,
scale: options.walletAddress.asset.scale
code: receiver.incomingAmount.assetCode,
scale: receiver.incomingAmount.assetScale
}
})
if (isConvertError(converted)) {
if (isConvertError(convertResults)) {
throw new PaymentMethodHandlerError(
'Received error during local quoting',
{
Expand All @@ -152,7 +160,8 @@ async function getQuote(
}
)
}
debitAmountValue = converted
debitAmountValue = convertResults.amount
exchangeRate = convertResults.scaledExchangeRate
} else {
throw new PaymentMethodHandlerError('Received error during local quoting', {
description: 'No value provided to get quote from',
Expand All @@ -178,8 +187,7 @@ async function getQuote(
return {
receiver: options.receiver,
walletAddress: options.walletAddress,
estimatedExchangeRate:
Number(receiveAmountValue) / Number(debitAmountValue),
estimatedExchangeRate: exchangeRate,
debitAmount: {
value: debitAmountValue,
assetCode: options.walletAddress.asset.code,
Expand Down
25 changes: 20 additions & 5 deletions packages/backend/src/rates/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,10 @@ describe('Rates service', function () {
sourceAsset: { code: 'USD', scale: 9 },
destinationAsset: { code: 'USD', scale: 9 }
})
).resolves.toBe(1234n)
).resolves.toEqual({
amount: 1234n,
scaledExchangeRate: 1
})
expect(apiRequestCount).toBe(0)
})

Expand All @@ -92,14 +95,20 @@ describe('Rates service', function () {
sourceAsset: { code: 'USD', scale: 9 },
destinationAsset: { code: 'USD', scale: 12 }
})
).resolves.toBe(123_000n)
).resolves.toEqual({
amount: 123_000n,
scaledExchangeRate: 1000
})
await expect(
service.convert({
sourceAmount: 123456n,
sourceAsset: { code: 'USD', scale: 12 },
destinationAsset: { code: 'USD', scale: 9 }
})
).resolves.toBe(123n)
).resolves.toEqual({
amount: 123n,
scaledExchangeRate: 0.001
})
expect(apiRequestCount).toBe(0)
})

Expand All @@ -111,14 +120,20 @@ describe('Rates service', function () {
sourceAsset: { code: 'USD', scale: 2 },
destinationAsset: { code: 'EUR', scale: 2 }
})
).resolves.toBe(BigInt(sourceAmount * exampleRates.USD.EUR))
).resolves.toEqual({
amount: BigInt(sourceAmount * exampleRates.USD.EUR),
scaledExchangeRate: exampleRates.USD.EUR
})
await expect(
service.convert({
sourceAmount: BigInt(sourceAmount),
sourceAsset: { code: 'EUR', scale: 2 },
destinationAsset: { code: 'USD', scale: 2 }
})
).resolves.toBe(BigInt(sourceAmount * exampleRates.EUR.USD))
).resolves.toEqual({
amount: BigInt(sourceAmount * exampleRates.EUR.USD),
scaledExchangeRate: exampleRates.EUR.USD
})
})

it('returns an error when an asset price is invalid', async () => {
Expand Down
48 changes: 35 additions & 13 deletions packages/backend/src/rates/service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BaseService } from '../shared/baseService'
import Axios, { AxiosInstance, isAxiosError } from 'axios'
import { convert, ConvertOptions } from './util'
import { convert, convertReverse, ConvertOptions, ConvertResults } from './util'
import { createInMemoryDataStore } from '../middleware/cache/data-stores/in-memory'
import { CacheDataStore } from '../middleware/cache/data-stores'

Expand All @@ -11,11 +11,13 @@ export interface Rates {
rates: Record<string, number>
}

export type RateConvertOpts = Omit<ConvertOptions, 'exchangeRate'>
export type RateConvertOpts = Omit<ConvertOptions, 'exchangeRate'> & {
reverseDirection?: boolean
}

export interface RatesService {
rates(baseAssetCode: string): Promise<Rates>
convert(opts: RateConvertOpts): Promise<bigint | ConvertError>
convert(opts: RateConvertOpts): Promise<ConvertResults | ConvertError>
}

interface ServiceDependencies extends BaseService {
Expand Down Expand Up @@ -51,22 +53,42 @@ class RatesServiceImpl implements RatesService {
this.cachedRates = createInMemoryDataStore(deps.exchangeRatesLifetime)
}

async convert(
opts: Omit<ConvertOptions, 'exchangeRate'>
): Promise<bigint | ConvertError> {
const sameCode = opts.sourceAsset.code === opts.destinationAsset.code
const sameScale = opts.sourceAsset.scale === opts.destinationAsset.scale
if (sameCode && sameScale) return opts.sourceAmount
if (sameCode) return convert({ exchangeRate: 1.0, ...opts })
async convert(opts: RateConvertOpts): Promise<ConvertResults | ConvertError> {
const {
reverseDirection = false,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

convert function is same as before except this reverseDirection controls which conversion function is used (convert or new convertReverse). Alternatively considered making a different ratesService.convertReverse - perhaps thats a bit more explicit but I didn't have a strong preference and found this to be simpler.

sourceAsset,
destinationAsset,
sourceAmount
} = opts

const sameCode = sourceAsset.code === destinationAsset.code
const sameScale = sourceAsset.scale === destinationAsset.scale
if (sameCode && sameScale)
return { amount: sourceAmount, scaledExchangeRate: 1 }

const conversionFn = reverseDirection ? convertReverse : convert

const { rates } = await this.getRates(opts.sourceAsset.code)
if (sameCode)
return conversionFn({
exchangeRate: 1.0,
sourceAsset,
destinationAsset,
sourceAmount
})

const destinationExchangeRate = rates[opts.destinationAsset.code]
const { rates } = await this.getRates(sourceAsset.code)

const destinationExchangeRate = rates[destinationAsset.code]
if (!destinationExchangeRate || !isValidPrice(destinationExchangeRate)) {
return ConvertError.InvalidDestinationPrice
}

return convert({ exchangeRate: destinationExchangeRate, ...opts })
return conversionFn({
exchangeRate: destinationExchangeRate,
sourceAsset,
destinationAsset,
sourceAmount
})
}

async rates(baseAssetCode: string): Promise<Rates> {
Expand Down
Loading
Loading