Skip to content

Commit

Permalink
refactor: rename prices -> exchange rates (#1351)
Browse files Browse the repository at this point in the history
* docs(backend): add exchange rate openapi spec

* refactor(backend): rename prices -> exchange rates

* fix(backend): formatting

* refactor(mase): prices -> rates
  • Loading branch information
sabineschaller authored Apr 21, 2023
1 parent 75fba84 commit e463e10
Show file tree
Hide file tree
Showing 16 changed files with 123 additions and 64 deletions.
2 changes: 1 addition & 1 deletion docs/connector.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The [`backend`](./architecture.md#backend) includes an [Interledger](./glossary.

The amounts of these packets are used to update account balances in [TigerBeetle](./glossary.md#tigerbeetle). (See [Accounts and Transfers](./accounts-and-transfers.md))

Amounts are adjusted based on the destination/outgoing account's asset and Rafiki's configured exchange [rates service](./integration.md#rates-prices).
Amounts are adjusted based on the destination/outgoing account's asset and Rafiki's configured exchange [rates service](./integration.md#exchange-rates).

## Packet Origination

Expand Down
4 changes: 2 additions & 2 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ $ helm install ...
| `PAYMENT_POINTER_URL` | `http://127.0.0.1:3001/.well-known/pay` | Rafiki instance internal payment pointer |
| `PAYMENT_POINTER_WORKERS` | `1` | number of workers processing payment pointer requests |
| `PAYMENT_POINTER_WORKER_IDLE` | `200` | milliseconds |
| `PRICES_LIFETIME` | `15_000` | milliseconds |
| `PRICES_URL` | `undefined` | endpoint on the Account Servicing Entity to request receiver fees |
| `EXCHANGE_RATES_LIFETIME` | `15_000` | milliseconds |
| `EXCHANGE_RATES_URL` | `undefined` | endpoint on the Account Servicing Entity to request receiver fees |
| `PRIVATE_KEY_FILE` | `undefined` | Rafiki instance client private key |
| `PUBLIC_HOST` | `http://127.0.0.1:3001` | (testing) public Host for Open Payments APIs |
| `QUOTE_LIFESPAN` | `5 * 60_000` | milliseconds |
Expand Down
8 changes: 4 additions & 4 deletions docs/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

Account Servicing Entities provide and maintain payment accounts. In order to make these accounts Interledger-enabled via Rafiki, they need to provide the following endpoints and services:

- prices (exchange rates)
- exchange rates
- fees
- [webhook events listener](#webhook-events-listener)
- [Identity Provider](#identity-provider)
Expand All @@ -15,7 +15,7 @@ Furthermore, each payment account managed by the Account Servicing Entity needs

Every Interledger payment is preceded with a quote that estimates the costs for transfering value from A to B. The Account Servicing Entity may charge fees on top of that for facilitating that transfer. How they structure those fees is completely up to the Account Servicing Entity.

### Rates (Prices)
### Exchange Rates

For the quoting to be successful, Rafiki needs to be provided with the current exchange rate by the Account Servicing Entity. The Account Servicing Entity needs to expose an endpoint that accepts a `GET` requests and responds as follows.

Expand All @@ -27,9 +27,9 @@ For the quoting to be successful, Rafiki needs to be provided with the current e
| `rates` | Object | Object containing `<asset_code : exchange_rate>` pairs, e.g. `{EUR: 1.1602}` |
| `rates.<asset_code>` | Number | exchange rate given `base` and `<asset_code>` |

The response status code for a successful request is a `200`. The `mock-account-servicing-entity` includes a [minimalistic example](../localenv/mock-account-servicing-entity/app/routes/prices.ts).
The response status code for a successful request is a `200`. The `mock-account-servicing-entity` includes a [minimalistic example](../localenv/mock-account-servicing-entity/app/routes/rates.ts).

The `backend` package requires an environment variable called `PRICES_URL` which MUST specify the URL of this endpoint.
The `backend` package requires an environment variable called `EXCHANGE_RATES_URL` which MUST specify the URL of this endpoint.

### Fees

Expand Down
2 changes: 1 addition & 1 deletion localenv/cloud-nine-wallet/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ services:
PUBLIC_HOST: http://cloud-nine-wallet-backend
OPEN_PAYMENTS_URL: http://cloud-nine-wallet-backend
WEBHOOK_URL: http://cloud-nine-wallet/webhooks
PRICES_URL: http://cloud-nine-wallet/prices
EXCHANGE_RATES_URL: http://cloud-nine-wallet/rates
REDIS_URL: redis://redis:6379/0
QUOTE_URL: http://cloud-nine-wallet/quotes
PAYMENT_POINTER_URL: https://cloud-nine-wallet-backend/.well-known/pay
Expand Down
2 changes: 1 addition & 1 deletion localenv/happy-life-bank/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ services:
PUBLIC_HOST: http://happy-life-bank-backend
WEBHOOK_URL: http://happy-life-bank/webhooks
OPEN_PAYMENTS_URL: http://happy-life-bank-backend
PRICES_URL: http://happy-life-bank/prices
EXCHANGE_RATES_URL: http://happy-life-bank/rates
REDIS_URL: redis://redis:6379/1
QUOTE_URL: http://happy-life-bank/quotes
PAYMENT_POINTER_URL: https://happy-life-bank-backend/.well-known/pay
Expand Down
4 changes: 2 additions & 2 deletions packages/backend/src/config/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ export const Config = {
? process.env.TIGERBEETLE_REPLICA_ADDRESSES.split(',')
: ['3004'],

pricesUrl: process.env.PRICES_URL, // optional
pricesLifetime: +(process.env.PRICES_LIFETIME || 15_000),
exchangeRatesUrl: process.env.EXCHANGE_RATES_URL, // optional
exchangeRatesLifetime: +(process.env.EXCHANGE_RATES_LIFETIME || 15_000),

slippage: envFloat('SLIPPAGE', 0.01),
quoteLifespan: envInt('QUOTE_LIFESPAN', 5 * 60_000), // milliseconds
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const RafikiServicesFactory = Factory.define<MockRafikiServices>(
}))
.attr('rates', {
convert: async (opts) => opts.sourceAmount,
prices: () => {
rates: () => {
throw new Error('unimplemented')
}
})
Expand Down
4 changes: 2 additions & 2 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,8 +311,8 @@ export function initIocContainer(
const config = await deps.use('config')
return createRatesService({
logger: await deps.use('logger'),
pricesUrl: config.pricesUrl,
pricesLifetime: config.pricesLifetime
exchangeRatesUrl: config.exchangeRatesUrl,
exchangeRatesLifetime: config.exchangeRatesLifetime
})
})

Expand Down
4 changes: 2 additions & 2 deletions packages/backend/src/open_payments/payment/outgoing/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export type PaymentError = LifecycleError | Pay.PaymentError

export enum LifecycleError {
// Rate fetch failed.
PricesUnavailable = 'PricesUnavailable',
RatesUnavailable = 'RatesUnavailable',
// Edge error due to retries, partial payment, and database write errors.
BadState = 'BadState',
// Account asset conflicts with sendAmount asset
Expand All @@ -63,7 +63,7 @@ export enum LifecycleError {

const retryablePaymentErrors: { [paymentError in PaymentError]?: boolean } = {
// Lifecycle errors
PricesUnavailable: true,
RatesUnavailable: true,
// From @interledger/pay's PaymentError:
ConnectorError: true,
EstablishmentFailed: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,8 @@ describe('OutgoingPaymentService', (): void => {
}

beforeAll(async (): Promise<void> => {
Config.pricesUrl = 'https://test.prices'
nock(Config.pricesUrl)
Config.exchangeRatesUrl = 'https://test.rates'
nock(Config.exchangeRatesUrl)
.get('/')
.reply(200, () => ({
base: 'USD',
Expand Down
8 changes: 4 additions & 4 deletions packages/backend/src/open_payments/quote/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ describe('QuoteService', (): void => {
}

beforeAll(async (): Promise<void> => {
Config.pricesUrl = 'https://test.prices'
Config.exchangeRatesUrl = 'https://test.rates'
Config.signatureSecret = SIGNATURE_SECRET
nock(Config.pricesUrl)
nock(Config.exchangeRatesUrl)
.get('/')
.reply(200, () => ({
base: 'USD',
Expand Down Expand Up @@ -670,7 +670,7 @@ describe('QuoteService', (): void => {
it('fails on rate service error', async (): Promise<void> => {
const ratesService = await deps.use('ratesService')
jest
.spyOn(ratesService, 'prices')
.spyOn(ratesService, 'rates')
.mockImplementation(() => Promise.reject(new Error('fail')))
const incomingPayment = await createIncomingPayment(deps, {
paymentPointerId: receivingPaymentPointer.id
Expand All @@ -682,7 +682,7 @@ describe('QuoteService', (): void => {
receiver: incomingPayment.getUrl(receivingPaymentPointer),
sendAmount
})
).rejects.toThrow('missing prices')
).rejects.toThrow('missing rates')
})
})
})
6 changes: 3 additions & 3 deletions packages/backend/src/open_payments/quote/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,8 @@ export async function startQuote(
deps: ServiceDependencies,
options: StartQuoteOptions
): Promise<Pay.Quote> {
const prices = await deps.ratesService.prices().catch((_err: Error) => {
throw new Error('missing prices')
const rates = await deps.ratesService.rates().catch((_err: Error) => {
throw new Error('missing rates')
})

const plugin = deps.makeIlpPlugin({
Expand All @@ -246,7 +246,7 @@ export async function startQuote(
const quote = await Pay.startQuote({
...quoteOptions,
slippage: deps.slippage,
prices
prices: rates
}).finally(() => {
return Pay.closeConnection(
quoteOptions.plugin,
Expand Down
59 changes: 59 additions & 0 deletions packages/backend/src/openapi/exchange-rates.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
openapi: 3.1.0
info:
title: Rafiki Exchange Rates
version: '1.0'
license:
name: Apache 2.0
identifier: Apache-2.0
summary: Integration Endpoint Rafiki expects at the Account Servicing Entity
description: 'Rafiki calls this endpoint at the Account Servicing Entity in order to fetch current exchange rates.'
contact:
email: tech@interledger.org
servers:
- url: 'https://account-servicing-entity.com/rates'
tags:
- name: rates
description: Exchange rates
paths:
/:
parameters: []
get:
summary: Fetch exchange rates
operationId: get-rates
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/rates'
examples:
Exchange Rates:
value:
base: 'USD'
rates:
EUR: 1.1602
ZAR: 17.3792
'404':
description: Not Found
description: Fetch current exchange rate pairs.
tags:
- rates
components:
schemas:
rates:
title: rates
type: object
properties:
base:
type: string
rates:
type: object
patternProperties:
^[A-Z]{3}$:
type: number
required:
- base
- rates
securitySchemes: {}
security: []
14 changes: 7 additions & 7 deletions packages/backend/src/rates/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('Rates service', function () {
let appContainer: TestContainer
let service: RatesService
let requestCount = 0
const pricesLifetime = 100
const exchangeRatesLifetime = 100
const koa = new Koa()
koa.use(function (ctx) {
requestCount++
Expand All @@ -30,8 +30,8 @@ describe('Rates service', function () {

beforeAll(async (): Promise<void> => {
const config = Config
config.pricesLifetime = pricesLifetime
config.pricesUrl = 'http://127.0.0.1:3210/'
config.exchangeRatesLifetime = exchangeRatesLifetime
config.exchangeRatesUrl = 'http://127.0.0.1:3210/'
deps = await initIocContainer(config)
appContainer = await createTestApp(deps)
jest.useFakeTimers()
Expand All @@ -40,7 +40,7 @@ describe('Rates service', function () {

beforeEach(async (): Promise<void> => {
// Fast-forward to reset the cache between tests.
jest.setSystemTime(Date.now() + pricesLifetime + 1)
jest.setSystemTime(Date.now() + exchangeRatesLifetime + 1)
service = await deps.use('ratesService')
requestCount = 0
})
Expand Down Expand Up @@ -157,7 +157,7 @@ describe('Rates service', function () {

it('prefetches when the cached request is old', async () => {
await expect(service.convert(opts)).resolves.toBe(1234n * 2n) // setup initial rate
jest.advanceTimersByTime(pricesLifetime * 0.5 + 1)
jest.advanceTimersByTime(exchangeRatesLifetime * 0.5 + 1)
// ... cache isn't expired yet, but it will be soon
await expect(service.convert(opts)).resolves.toBe(1234n * 2n)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -166,7 +166,7 @@ describe('Rates service', function () {
expect(requestCount).toBe(1)

// Invalidate the cache.
jest.advanceTimersByTime(pricesLifetime * 0.5 + 1)
jest.advanceTimersByTime(exchangeRatesLifetime * 0.5 + 1)
await expect(service.convert(opts)).resolves.toBe(1234n * 2n)
// The prefetch response is promoted to the cache.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -176,7 +176,7 @@ describe('Rates service', function () {

it('cannot use an expired cache', async () => {
await expect(service.convert(opts)).resolves.toBe(1234n * 2n) // setup initial rate
jest.advanceTimersByTime(pricesLifetime + 1)
jest.advanceTimersByTime(exchangeRatesLifetime + 1)
await expect(service.convert(opts)).resolves.toBe(1234n * 2n)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((<any>service).prefetchRequest).toBeUndefined()
Expand Down
Loading

0 comments on commit e463e10

Please sign in to comment.