Skip to content

Commit

Permalink
adding replyTo field in http request body to submit RFQ (#142)
Browse files Browse the repository at this point in the history
* changing rfqdata fields per spec change. still need to merge schema and test vector change in tbdex repo and update the tbdex submodule

* backing out protocol changes

* adding replyTo in req body

* adding a test to include replyTo field when sending an rfq

* changed file names to align with http spec. added valid url check in create exchange handler method, wrote a test against this url check

* adding back an xit test case

* remove alias

* adding a replyTo field check

* add test for throwing error if order is sent with replyTo field

* removing .onlys from test

* adding doc to dev tool method

* building requestBody differently for rfq vs other messages. writing a doc for replyTo field in SendMessagesOpts

* trust fall exercise with typescript type. removing the paranoid rfq message type check

* renaming a variable

* renaming

* vcjwt is a string
  • Loading branch information
jiyoontbd authored Feb 5, 2024
1 parent 53a8b5d commit 85e5841
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 28 deletions.
16 changes: 14 additions & 2 deletions packages/http-client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ export class TbdexHttpClient {
* @throws if recipient DID does not have a PFI service entry
*/
static async sendMessage<T extends MessageKind>(opts: SendMessageOptions<T>): Promise<void> {
const { message } = opts
const { message, replyTo } = opts

const jsonMessage: MessageModel<T> = message instanceof Message ? message.toJSON() : message

await Message.verify(jsonMessage)
Expand All @@ -76,10 +77,16 @@ export class TbdexHttpClient {

let response: Response
try {
let requestBody
if (jsonMessage.metadata.kind == 'rfq') {
requestBody = JSON.stringify({ rfq: jsonMessage, replyTo})
} else {
requestBody = JSON.stringify(jsonMessage)
}
response = await fetch(apiRoute, {
method : 'POST',
headers : { 'content-type': 'application/json' },
body : JSON.stringify(jsonMessage)
body : requestBody
})
} catch (e) {
throw new RequestError({ message: `Failed to send message to ${pfiDid}`, recipientDid: pfiDid, url: apiRoute, cause: e })
Expand Down Expand Up @@ -362,6 +369,11 @@ export class TbdexHttpClient {
export type SendMessageOptions<T extends MessageKind> = {
/** the message you want to send */
message: Message<T> | MessageModel<T>
/**
* A string containing a valid URI where new messages from the PFI will be sent.
* This field is only available as an option when sending an RFQ Message.
*/
replyTo?: T extends 'rfq' ? string : never
}

/**
Expand Down
28 changes: 18 additions & 10 deletions packages/http-client/tests/client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
RequestTokenVerificationError,
RequestTokenSigningError
} from '../src/errors/index.js'
import { DevTools, Message } from '@tbdex/protocol'
import { DevTools, Message, Rfq } from '@tbdex/protocol'
import * as sinon from 'sinon'
import { JwtHeaderParams, JwtPayload, PrivateKeyJwk, Secp256k1 } from '@web5/crypto'
import { Convert } from '@web5/common'
Expand All @@ -31,15 +31,11 @@ sinon.stub(Message, 'verify').resolves('123')
describe('client', () => {
beforeEach(() => getPfiServiceEndpointStub.resolves('https://localhost:9000'))

describe('sendMessage', () => {
describe('sendMessage', async () => {
let mockRfq: Rfq

let mockRfq: Message<'rfq'>
beforeEach(async () =>
{
mockRfq = await DevTools.createRfq({
sender : await DevTools.createDid(),
receiver : dhtDid
})
beforeEach(async () => {
mockRfq = await DevTools.createRfq({ sender: dhtDid, receiver: dhtDid })
})

it('throws RequestError if service endpoint url is garbage', async () => {
Expand Down Expand Up @@ -79,7 +75,19 @@ describe('client', () => {
expect(e.url).to.equal(`https://localhost:9000/exchanges/${mockRfq.metadata.exchangeId}/rfq`)
}
})
it('should not throw errors if all is well', async () => {
it('should not throw errors if all is well when sending RFQ with replyTo field', async () => {
fetchStub.resolves({
ok : true,
json : () => Promise.resolve()
} as Response)

try {
await TbdexHttpClient.sendMessage({message: mockRfq, replyTo: 'https://tbdex.io/callback'})
} catch (e) {
expect.fail()
}
})
it('should not throw errors if all is well when sending RFQ without replyTo field', async () => {
fetchStub.resolves({
ok : true,
json : () => Promise.resolve()
Expand Down
4 changes: 2 additions & 2 deletions packages/http-server/src/http-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type { Express } from 'express'
import express from 'express'
import cors from 'cors'

import { getExchanges, getOfferings, submitOrder, submitClose, submitRfq } from './request-handlers/index.js'
import { getExchanges, getOfferings, submitOrder, submitClose, createExchange } from './request-handlers/index.js'
import { jsonBodyParser } from './middleware/index.js'
import { fakeExchangesApi, fakeOfferingsApi } from './fakes.js'

Expand Down Expand Up @@ -125,7 +125,7 @@ export class TbdexHttpServer {
listen(port: number | string, callback?: () => void) {
const { offeringsApi, exchangesApi, pfiDid } = this

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,23 @@ import type { ErrorDetail } from '@tbdex/http-client'
import { Message } from '@tbdex/protocol'
import { CallbackError } from '../callback-error.js'

type SubmitRfqOpts = {
type CreateExchangeOpts = {
callback: SubmitCallback<'rfq'>
offeringsApi: OfferingsApi
exchangesApi: ExchangesApi
}

export function submitRfq(options: SubmitRfqOpts): RequestHandler {
export function createExchange(options: CreateExchangeOpts): RequestHandler {
const { offeringsApi, exchangesApi, callback } = options
return async function (req, res) {
let message: Message<MessageKind>

if (req.body.replyTo && !isValidUrl(req.body.replyTo)) {
return res.status(400).json({ errors: [{ detail: 'replyTo must be a valid url' }] })
}

try {
message = await Message.parse(req.body)
message = await Message.parse(req.body.rfq)
} catch(e) {
const errorResponse: ErrorDetail = { detail: `Parsing of TBDex message failed: ${e.message}` }
return res.status(400).json({ errors: [errorResponse] })
Expand Down Expand Up @@ -67,3 +71,12 @@ export function submitRfq(options: SubmitRfqOpts): RequestHandler {
return res.sendStatus(202)
}
}

function isValidUrl(replyToUrl: string) {
try {
new URL(replyToUrl)
return true
} catch (err) {
return false
}
}
2 changes: 1 addition & 1 deletion packages/http-server/src/request-handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ export * from './get-exchanges.js'
export * from './get-offerings.js'
export * from './submit-close.js'
export * from './submit-order.js'
export * from './submit-rfq.js'
export * from './create-exchange.js'
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ErrorDetail } from '@tbdex/http-client'
import type { Server } from 'http'

import { TbdexHttpServer } from '../src/main.js'
import { DevTools, TbdexHttpServer } from '../src/main.js'
import { expect } from 'chai'

let api = new TbdexHttpServer()
Expand Down Expand Up @@ -48,6 +48,27 @@ describe('POST /exchanges/:exchangeId/rfq', () => {
expect(error.detail).to.include('JSON')
})

it('returns a 400 if create exchange request contains a replyTo which is not a valid URL', async () => {
const aliceDid = await DevTools.createDid()
const pfiDid = await DevTools.createDid()
const rfq = await DevTools.createRfq({ sender: aliceDid, receiver: pfiDid })
await rfq.sign(aliceDid)

const resp = await fetch('http://localhost:8000/exchanges/123/rfq', {
method : 'POST',
body : JSON.stringify({ rfq: rfq, replyTo: 'foo'})
})

expect(resp.status).to.equal(400)

const responseBody = await resp.json() as { errors: ErrorDetail[] }
expect(responseBody.errors.length).to.equal(1)

const [ error ] = responseBody.errors
expect(error.detail).to.exist
expect(error.detail).to.include('replyTo must be a valid url')
})

xit('returns a 400 if request body is not a valid RFQ')
xit('returns a 400 if request body if integrity check fails')
xit('returns a 409 if request body if RFQ already exists')
Expand Down
37 changes: 28 additions & 9 deletions packages/protocol/src/dev-tools.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@

import type { OfferingData, QuoteData, RfqData } from './types.js'
import type { PortableDid } from '@web5/dids'

import { DidDhtMethod, DidIonMethod, DidKeyMethod } from '@web5/dids'
import { VerifiableCredential } from '@web5/credentials'
import { Offering } from './resource-kinds/index.js'
import { Rfq } from './message-kinds/index.js'
import { Order, Rfq } from './message-kinds/index.js'
import { Resource } from './resource.js'
import { Message } from './main.js'
import { VerifiableCredential } from '@web5/credentials'

/**
* Supported DID Methods
* @beta
*/
export type DidMethodOptions = 'key' | 'ion'
export type DidMethodOptions = 'key' | 'ion' | 'dht'

/**
* Options passed to {@link DevTools.createRfq}
* @beta
*/
export type RfqOptions = {
export type MessageOptions = {
/**
* {@link @web5/dids#PortableDid} of the rfq sender. used to generate a random credential that fulfills the vcRequirements
* {@link @web5/dids#PortableDid} of the message sender. When generating RFQ, it is used to generate a random credential that fulfills the vcRequirements
* of the offering returned by {@link DevTools.createOffering}
*/
sender: PortableDid
Expand All @@ -42,9 +44,9 @@ export class DevTools {
if (didMethod === 'key') {
return await DidKeyMethod.create()
} else if (didMethod === 'ion') {
return await DidIonMethod.create()
return DidIonMethod.create()
} else if (didMethod === 'dht') {
return await DidDhtMethod.create()
return DidDhtMethod.create()
} else {
throw new Error(`${didMethod} method not implemented.`)
}
Expand Down Expand Up @@ -174,7 +176,7 @@ export class DevTools {
*
* **NOTE**: generates a random credential that fulfills the offering's required claims
*/
static async createRfq(opts: RfqOptions) {
static async createRfq(opts: MessageOptions) {
const { sender, receiver } = opts

const rfqData: RfqData = await DevTools.createRfqData(opts)
Expand All @@ -185,10 +187,27 @@ export class DevTools {
})
}

/**
* Creates and returns an example Order with a generated exchangeId. Useful for testing purposes
* @param opts - options used to create a Message
* @returns Order message
*/
static createOrder(opts: MessageOptions) {
const { sender, receiver } = opts

return Order.create({
metadata: {
from : sender.did,
to : receiver?.did ?? 'did:ex:pfi',
exchangeId : Message.generateId('rfq')
}
})
}

/**
* creates an example RfqData. Useful for testing purposes
*/
static async createRfqData(opts?: RfqOptions): Promise<RfqData> {
static async createRfqData(opts?: MessageOptions): Promise<RfqData> {
let vcJwt: string = ''

if (opts?.sender) {
Expand Down

0 comments on commit 85e5841

Please sign in to comment.