Skip to content
This repository has been archived by the owner on Dec 12, 2024. It is now read-only.

Commit

Permalink
Stricten, test, and bugfix http-server (#170)
Browse files Browse the repository at this point in the history
* Stricten, test, and bugfix http-server

* PR comments

* Update packages/http-server/tests/submit-close.spec.ts

Co-authored-by: Jiyoon Koo <[email protected]>

---------

Co-authored-by: Jiyoon Koo <[email protected]>
  • Loading branch information
Diane Huxley and jiyoonie9 authored Feb 16, 2024
1 parent 0702d91 commit ebefc54
Show file tree
Hide file tree
Showing 22 changed files with 1,489 additions and 378 deletions.
3 changes: 3 additions & 0 deletions packages/http-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,15 @@
},
"devDependencies": {
"@types/chai": "4.3.6",
"@types/cors": "^2.8.17",
"@types/express": "4.17.17",
"@types/http-errors": "2.0.4",
"@types/mocha": "10.0.1",
"@types/node": "20.9.4",
"@types/sinon": "^17.0.3",
"chai": "4.3.10",
"rimraf": "5.0.1",
"sinon": "17.0.1",
"supertest": "6.3.3",
"typescript": "5.2.2"
},
Expand Down
58 changes: 0 additions & 58 deletions packages/http-server/src/fakes.ts

This file was deleted.

64 changes: 41 additions & 23 deletions packages/http-server/src/http-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import type {
GetOfferingsCallback,
} from './types.js'

import type { Express } from 'express'
import type { Express, Request, Response } from 'express'

import express from 'express'
import cors from 'cors'

import { getExchanges, getOfferings, submitOrder, submitClose, createExchange } from './request-handlers/index.js'
import { jsonBodyParser } from './middleware/index.js'
import { fakeExchangesApi, fakeOfferingsApi } from './fakes.js'
import { InMemoryOfferingsApi } from './in-memory-offerings-api.js'
import { InMemoryExchangesApi } from './in-memory-exchanges-api.js'

/**
* Maps the requests to their respective callbacks handlers
Expand Down Expand Up @@ -72,8 +73,8 @@ export class TbdexHttpServer {
constructor(opts?: NewHttpServerOptions) {
this.callbacks = {}

this.exchangesApi = opts?.exchangesApi ?? fakeExchangesApi
this.offeringsApi = opts?.offeringsApi ?? fakeOfferingsApi
this.exchangesApi = opts?.exchangesApi ?? new InMemoryExchangesApi()
this.offeringsApi = opts?.offeringsApi ?? new InMemoryOfferingsApi()
this.pfiDid = opts?.pfiDid ?? 'did:ex:pfi'

// initialize api here so that consumers can attach custom endpoints
Expand Down Expand Up @@ -139,25 +140,42 @@ export class TbdexHttpServer {
listen(port: number | string, callback?: () => void) {
const { offeringsApi, exchangesApi, pfiDid } = this

this.api.post('/exchanges/:exchangeId/rfq', createExchange({
callback: this.callbacks['rfq'], offeringsApi, exchangesApi,
}))

this.api.post('/exchanges/:exchangeId/order', submitOrder({
callback: this.callbacks['order'], exchangesApi
}))

this.api.post('/exchanges/:exchangeId/close', submitClose({
callback: this.callbacks['close'], exchangesApi
}))

this.api.get('/exchanges', getExchanges({
callback: this.callbacks['exchanges'], exchangesApi, pfiDid
}))

this.api.get('/offerings', getOfferings({
callback: this.callbacks['offerings'], offeringsApi
}))
this.api.post('/exchanges/:exchangeId/rfq', (req: Request, res: Response) =>
createExchange(req, res, {
callback: this.callbacks['rfq'],
offeringsApi,
exchangesApi,
})
)

this.api.post('/exchanges/:exchangeId/order', (req: Request, res: Response) =>
submitOrder(req, res, {
callback: this.callbacks['order'],
exchangesApi
})
)

this.api.post('/exchanges/:exchangeId/close', (req: Request, res: Response) =>
submitClose(req, res,{
callback: this.callbacks.close,
exchangesApi,
})
)

this.api.get('/exchanges', (req: Request, res: Response) =>
getExchanges(req, res, {
callback: this.callbacks.exchanges,
exchangesApi,
pfiDid,
})
)

this.api.get('/offerings', (req, res) =>
getOfferings(req, res, {
callback: this.callbacks['offerings'],
offeringsApi
})
)

// TODO: support hostname and backlog arguments
return this.api.listen(port, callback)
Expand Down
89 changes: 89 additions & 0 deletions packages/http-server/src/in-memory-exchanges-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Exchange } from '@tbdex/protocol'
import { Message, Rfq, Quote, Order, OrderStatus, Close } from '@tbdex/protocol'
import { ExchangesApi, GetExchangesFilter } from './main.js'

/**
* An in-memory implementation of {@link ExchangesApi} for example and default purposes.
* InMemoryExchangesApi has additional methods {@link InMemoryExchangesApi.addMessage}
* and {@link InMemoryExchangesApi.clearMessages}
*/
export class InMemoryExchangesApi implements ExchangesApi {
/** Map from exchange_id to Exchange */
exchangeMessagesMap: Map<string, Exchange>

constructor() {
this.exchangeMessagesMap = new Map<string, Exchange>()
}

async getExchanges(opts?: { filter: GetExchangesFilter }): Promise<Exchange[]> {
if (opts === undefined || opts.filter === undefined) {
// In production, this should probably return an empty list.
// For example and testing purposes, we return all exchanges.

return Array.from(this.exchangeMessagesMap.values())
}

const exchanges: Exchange[] = []
if (opts.filter.id) {
// filter has `id` and `from`

for (const id of opts.filter.id) {
const exchange = this.exchangeMessagesMap.get(id)
if (exchange?.rfq?.from === opts.filter.from) {
exchanges.push(exchange)
}
}
} else {
// filter only has `from`
this.exchangeMessagesMap.forEach((exchange, _id) => {
// You definitely shouldn't use InMemoryExchangesApi in production.
// This will get really slow
if (exchange?.rfq?.from === opts.filter.from) {
exchanges.push(exchange)
}
})
}

return exchanges
}

async getExchange(opts: { id: string} ): Promise<Exchange | undefined> {
const exchange = this.exchangeMessagesMap.get(opts.id)
return Promise.resolve(exchange)
}

async getRfq(opts: { exchangeId: string }): Promise<Rfq | undefined> {
const exchange = this.exchangeMessagesMap.get(opts.exchangeId)
return exchange?.rfq
}

async getQuote(opts: { exchangeId: string }): Promise<Quote | undefined> {
const exchange = this.exchangeMessagesMap.get(opts.exchangeId)
return Promise.resolve(exchange?.quote)
}

async getOrder(opts: { exchangeId: string }): Promise<Order | undefined> {
const exchange = this.exchangeMessagesMap.get(opts.exchangeId)
return exchange?.order
}

async getOrderStatuses(opts: { exchangeId: string }): Promise<OrderStatus[]> {
const exchange = this.exchangeMessagesMap.get(opts.exchangeId)
return exchange?.orderstatus ?? []
}

async getClose(opts: { exchangeId: string }): Promise<Close | undefined> {
const exchange = this.exchangeMessagesMap.get(opts.exchangeId)
return exchange?.close
}

addMessage(message: Message): void {
const exchange = this.exchangeMessagesMap.get(message.exchangeId) ?? new Exchange()
exchange.addNextMessage(message)
this.exchangeMessagesMap.set(message.exchangeId, exchange)
}

clearMessages(): void {
this.exchangeMessagesMap = new Map<string, Exchange>()
}
}
72 changes: 72 additions & 0 deletions packages/http-server/src/in-memory-offerings-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Offering } from '@tbdex/protocol'
import { GetOfferingsFilter, OfferingsApi } from './types.js'

/**
* An in-memory implementation of {@link OfferingsApi} for example and default purposes.
* InMemoryOfferingsApi has additional methods {@link InMemoryOfferingsApi.addOffering}
* and {@link InMemoryOfferingsApi.clearOfferings}
*/
export class InMemoryOfferingsApi implements OfferingsApi {
/** Map from offering_id to Offering */
offeringsMap: Map<string, Offering>

constructor() {
this.offeringsMap = new Map<string, Offering>()
}

/**
* Add a single offering
* @param offering - Offering to be added to the {@link offeringsMap}
*/
addOffering(offering: Offering): void {
this.offeringsMap.set(offering.metadata.id, offering)
}

/**
* Clear existing list offerings
*/
clearOfferings(): void {
this.offeringsMap.clear()
}

/**
* Retrieve a single offering if found
* @param opts - Filter with id used to select an offering
* @returns An offering if one exists, else undefined
*/
async getOffering(opts: { id: string }): Promise<Offering | undefined>{
return this.offeringsMap.get(opts.id)
}

/**
*
* @param opts - Filter used to select offerings
* @returns A list of offerings matching the filter
*/
async getOfferings(opts?: { filter: GetOfferingsFilter }): Promise<Offering[]> {
const allOfferings = Array.from(this.offeringsMap.values())

if (opts?.filter === undefined || Object.values(opts.filter).every(v => v === undefined)) {
// If no filter is provided, return all offerings
return allOfferings
}

const { filter: {
id,
payinCurrency,
payoutCurrency,
payinMethodKind,
payoutMethodKind,
} } = opts

return allOfferings.filter((offering) => {
// If filter includes a field, make sure the returned offerings match
return (!id || id === offering.metadata.id) &&
(!payinCurrency || payinCurrency === offering.data.payinCurrency.currencyCode) &&
(!payoutCurrency || payoutCurrency === offering.data.payoutCurrency.currencyCode) &&
(!payinMethodKind || offering.data.payinMethods.map(pm => pm.kind).includes(payinMethodKind)) &&
(!payoutMethodKind || offering.data.payoutMethods.map(pm => pm.kind).includes(payoutMethodKind))
})
}

}
Loading

0 comments on commit ebefc54

Please sign in to comment.