From 46481e7fe74111f4f8a3cc7bf3f7943843a30cf7 Mon Sep 17 00:00:00 2001 From: kirahsapong <102400653+kirahsapong@users.noreply.github.com> Date: Wed, 13 Mar 2024 07:56:06 -0700 Subject: [PATCH] Add protocol version (#191) * add required field protocol to message and resource * add checks for protocol version to create or add to exchange * remove jwk from generate-test-vectors * update submodule version Co-authored-by: Phoebe Lew --- .changeset/cyan-wolves-draw.md | 7 ++ .changeset/stale-hats-run.md | 5 ++ package.json | 2 +- packages/protocol/src/exchange.ts | 20 +++++- packages/protocol/src/message-kinds/close.ts | 5 +- .../src/message-kinds/order-status.ts | 5 +- packages/protocol/src/message-kinds/order.ts | 5 +- packages/protocol/src/message-kinds/quote.ts | 6 +- packages/protocol/src/message-kinds/rfq.ts | 9 ++- packages/protocol/src/message.ts | 5 ++ .../protocol/src/resource-kinds/offering.ts | 5 +- packages/protocol/src/resource.ts | 10 ++- packages/protocol/src/types.ts | 4 ++ packages/protocol/tests/close.spec.ts | 10 +++ packages/protocol/tests/exchange.spec.ts | 65 +++++++++++++++++-- .../protocol/tests/generate-test-vectors.ts | 19 +++--- packages/protocol/tests/rfq.spec.ts | 23 +++++++ tbdex | 2 +- 18 files changed, 172 insertions(+), 35 deletions(-) create mode 100644 .changeset/cyan-wolves-draw.md create mode 100644 .changeset/stale-hats-run.md diff --git a/.changeset/cyan-wolves-draw.md b/.changeset/cyan-wolves-draw.md new file mode 100644 index 00000000..c123432d --- /dev/null +++ b/.changeset/cyan-wolves-draw.md @@ -0,0 +1,7 @@ +--- +"@tbdex/protocol": minor +"@tbdex/http-client": patch +"@tbdex/http-server": patch +--- + +Introduce protocol field to messages and resources diff --git a/.changeset/stale-hats-run.md b/.changeset/stale-hats-run.md new file mode 100644 index 00000000..6cde34e1 --- /dev/null +++ b/.changeset/stale-hats-run.md @@ -0,0 +1,5 @@ +--- +"@tbdex/http-client": patch +--- + +Update client test diff --git a/package.json b/package.json index f03c8f0d..e09b9538 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "private": true, "type": "module", "scripts": { - "clean": "npkill -d $(pwd) -t node_modules && npkill -d $(pwd)/packages -t dist", + "clean": "npkill -d $(pwd)/packages -t dist && npkill -d $(pwd) -t node_modules", "build": "pnpm recursive run build", "lint": "pnpm recursive run lint", "lint:fix": "pnpm recursive run lint:fix", diff --git a/packages/protocol/src/exchange.ts b/packages/protocol/src/exchange.ts index 391154c3..247a3c15 100644 --- a/packages/protocol/src/exchange.ts +++ b/packages/protocol/src/exchange.ts @@ -49,9 +49,18 @@ export class Exchange { /** * Add the next message in the exchange * @param message - The next allowed message in the exchange + * @throws if message's protocol version does not match protocol version of other messages in the exchange * @throws if message is not a valid next message. See {@link Exchange.isValidNext} + * @throws if message's exchangeId does not match id of the exchange */ addNextMessage(message: Message): void { + if (this.protocol !== undefined && message.metadata.protocol !== this.protocol) { + throw new Error( + `Could not add message (${message.metadata.id}) with protocol version ${message.metadata.protocol} to exchange because it does not have matching ` + + `protocol version ${this.protocol} as other messages in the exchange` + ) + } + if (!this.isValidNext(message.metadata.kind)) { throw new Error( `Could not add message (${message.metadata.id}) to exchange because ${message.metadata.kind} ` + @@ -61,8 +70,8 @@ export class Exchange { if (this.exchangeId !== undefined && message.metadata.exchangeId !== this.exchangeId) { throw new Error( - `Could not add message with id ${message.metadata.id} to exchange because it does not have matching ` + - `exchange id ${this.exchangeId}` + `Could not add message (${message.metadata.id}) with exchange id ${message.metadata.exchangeId} to exchange because it does not have matching ` + + `exchange id ${this.exchangeId} as the exchange` ) } @@ -110,6 +119,13 @@ export class Exchange { return this.rfq?.metadata?.exchangeId } + /** + * The protocol version of all messages in the Exchange + */ + get protocol(): string | undefined { + return this.rfq?.metadata?.protocol + } + /** * A sorted list of messages currently in the exchange. */ diff --git a/packages/protocol/src/message-kinds/close.ts b/packages/protocol/src/message-kinds/close.ts index a043c1bc..ac0563dc 100644 --- a/packages/protocol/src/message-kinds/close.ts +++ b/packages/protocol/src/message-kinds/close.ts @@ -8,7 +8,7 @@ import { Parser } from '../parser.js' */ export type CreateCloseOptions = { data: CloseData - metadata: Omit + metadata: Omit & { protocol?: CloseMetadata['protocol'] } } /** @@ -60,7 +60,8 @@ export class Close extends Message { ...opts.metadata, kind : 'close', id : Message.generateId('close'), - createdAt : new Date().toISOString() + createdAt : new Date().toISOString(), + protocol : opts.metadata.protocol ?? '1.0' } const close = new Close(metadata, opts.data) diff --git a/packages/protocol/src/message-kinds/order-status.ts b/packages/protocol/src/message-kinds/order-status.ts index 798fd8c4..5ab6919a 100644 --- a/packages/protocol/src/message-kinds/order-status.ts +++ b/packages/protocol/src/message-kinds/order-status.ts @@ -8,7 +8,7 @@ import { Parser } from '../parser.js' */ export type CreateOrderStatusOptions = { data: OrderStatusData - metadata: Omit + metadata: Omit & { protocol?: OrderStatusMetadata['protocol'] } } /** @@ -61,7 +61,8 @@ export class OrderStatus extends Message { ...opts.metadata, kind : 'orderstatus', id : Message.generateId('orderstatus'), - createdAt : new Date().toISOString() + createdAt : new Date().toISOString(), + protocol : opts.metadata.protocol ?? '1.0' } const orderStatus = new OrderStatus(metadata, opts.data) diff --git a/packages/protocol/src/message-kinds/order.ts b/packages/protocol/src/message-kinds/order.ts index 1d0f781a..2a153ea4 100644 --- a/packages/protocol/src/message-kinds/order.ts +++ b/packages/protocol/src/message-kinds/order.ts @@ -7,7 +7,7 @@ import { Parser } from '../parser.js' * @beta */ export type CreateOrderOptions = { - metadata: Omit + metadata: Omit & { protocol?: OrderMetadata['protocol'] } } /** @@ -59,7 +59,8 @@ export class Order extends Message { ...opts.metadata, kind : 'order', id : Message.generateId('order'), - createdAt : new Date().toISOString() + createdAt : new Date().toISOString(), + protocol : opts.metadata.protocol ?? '1.0' } const order = new Order(metadata, {}) diff --git a/packages/protocol/src/message-kinds/quote.ts b/packages/protocol/src/message-kinds/quote.ts index 30f2df0f..fdab9522 100644 --- a/packages/protocol/src/message-kinds/quote.ts +++ b/packages/protocol/src/message-kinds/quote.ts @@ -8,7 +8,7 @@ import { Parser } from '../parser.js' */ export type CreateQuoteOptions = { data: QuoteData - metadata: Omit + metadata: Omit & { protocol?: QuoteMetadata['protocol'] } } /** @@ -64,8 +64,10 @@ export class Quote extends Message { ...opts.metadata, kind : 'quote', id : Message.generateId('quote'), - createdAt : new Date().toISOString() + createdAt : new Date().toISOString(), + protocol : opts.metadata.protocol ?? '1.0' } + const quote = new Quote(metadata, opts.data) quote.validateData() return quote diff --git a/packages/protocol/src/message-kinds/rfq.ts b/packages/protocol/src/message-kinds/rfq.ts index 773e817d..1f823121 100644 --- a/packages/protocol/src/message-kinds/rfq.ts +++ b/packages/protocol/src/message-kinds/rfq.ts @@ -13,7 +13,7 @@ import { Parser } from '../parser.js' */ export type CreateRfqOptions = { data: RfqData - metadata: Omit + metadata: Omit & { protocol?: RfqMetadata['protocol'] } } /** @@ -68,7 +68,8 @@ export class Rfq extends Message { kind : 'rfq', id : id, exchangeId : id, - createdAt : new Date().toISOString() + createdAt : new Date().toISOString(), + protocol : opts.metadata.protocol ?? '1.0' } // TODO: hash and set private fields @@ -95,6 +96,10 @@ export class Rfq extends Message { * @throws if payoutMethod in {@link Rfq.data} property `paymentDetails` cannot be validated against the provided offering's payoutMethod requiredPaymentDetails */ async verifyOfferingRequirements(offering: Offering) { + if (offering.metadata.protocol !== this.metadata.protocol) { + throw new Error(`protocol version mismatch. (rfq) ${this.metadata.protocol} !== ${offering.metadata.protocol} (offering)`) + } + if (offering.metadata.id !== this.data.offeringId) { throw new Error(`offering id mismatch. (rfq) ${this.data.offeringId} !== ${offering.metadata.id} (offering)`) } diff --git a/packages/protocol/src/message.ts b/packages/protocol/src/message.ts index 0c7237f1..6764209a 100644 --- a/packages/protocol/src/message.ts +++ b/packages/protocol/src/message.ts @@ -153,6 +153,11 @@ export abstract class Message { return this.metadata.externalId } + /** the protocol version */ + get protocol() { + return this.metadata.protocol + } + /** Rfq type guard */ isRfq(): this is Rfq { return this.metadata.kind === 'rfq' diff --git a/packages/protocol/src/resource-kinds/offering.ts b/packages/protocol/src/resource-kinds/offering.ts index c4ef45f7..6bd5b769 100644 --- a/packages/protocol/src/resource-kinds/offering.ts +++ b/packages/protocol/src/resource-kinds/offering.ts @@ -8,7 +8,7 @@ import { Parser } from '../parser.js' */ export type CreateOfferingOptions = { data: OfferingData - metadata: Omit + metadata: Omit & { protocol?: OfferingMetadata['protocol'] } } /** @@ -59,7 +59,8 @@ export class Offering extends Resource { ...opts.metadata, kind : 'offering', id : Resource.generateId('offering'), - createdAt : new Date().toISOString() + createdAt : new Date().toISOString(), + protocol : opts.metadata.protocol ?? '1.0' } const offering = new Offering(metadata, opts.data) diff --git a/packages/protocol/src/resource.ts b/packages/protocol/src/resource.ts index adea113c..80bf84c1 100644 --- a/packages/protocol/src/resource.ts +++ b/packages/protocol/src/resource.ts @@ -6,7 +6,6 @@ import { Crypto } from './crypto.js' import { validate } from './validator.js' import { BearerDid } from '@web5/dids' - /** * tbDEX Resources are published by PFIs for anyone to consume and generally used as a part of the discovery process. * They are not part of the message exchange, i.e Alice cannot reply to a Resource. @@ -42,7 +41,7 @@ export abstract class Resource { } /** - * Signs the message as a jws with detached content and sets the signature property + * Signs the resource as a jws with detached content and sets the signature property * @param did - the signer's DID * @throws If the signature could not be produced */ @@ -118,7 +117,7 @@ export abstract class Resource { } /** - * returns the message as a json object. Automatically used by `JSON.stringify` method. + * returns the resource as a json object. Automatically used by `JSON.stringify` method. */ toJSON(): ResourceModel { return { @@ -153,6 +152,11 @@ export abstract class Resource { return this.metadata.updatedAt } + /** the protocol version */ + get protocol() { + return this.metadata.protocol + } + /** offering type guard */ isOffering(): this is Offering { return this.metadata.kind === 'offering' diff --git a/packages/protocol/src/types.ts b/packages/protocol/src/types.ts index da2debd1..a46898aa 100644 --- a/packages/protocol/src/types.ts +++ b/packages/protocol/src/types.ts @@ -31,6 +31,8 @@ export type ResourceMetadata = { createdAt: string /** When the resource was last updated. Expressed as ISO8601 */ updatedAt?: string + /** Version of the protocol in use (x.x format). Any exchanges based off this resource must use the same version. */ + protocol: `${number}` } /** @@ -130,6 +132,8 @@ export type MessageMetadata = { createdAt: string /** Arbitrary ID for the caller to associate with the message. Optional */ externalId?: string + /** Version of the protocol in use (x.x format). Must be consistent with all other messages in a given exchange */ + protocol: `${number}` } /** diff --git a/packages/protocol/tests/close.spec.ts b/packages/protocol/tests/close.spec.ts index 717f0251..6e29740c 100644 --- a/packages/protocol/tests/close.spec.ts +++ b/packages/protocol/tests/close.spec.ts @@ -13,5 +13,15 @@ describe('Close', () => { expect(closeMessage.exchangeId).to.equal(exchangeId) expect(closeMessage.externalId).to.equal('ext_1234') }) + + it('sets `protocol` to current package version', () => { + const exchangeId = Message.generateId('rfq') + const closeMessage = Close.create({ + metadata : { from: 'did:ex:alice', to: 'did:ex:pfi', exchangeId, externalId: 'ext_1234' }, + data : { reason: 'beepboop' } + }) + + expect(closeMessage.protocol).to.equal('1.0') + }) }) }) \ No newline at end of file diff --git a/packages/protocol/tests/exchange.spec.ts b/packages/protocol/tests/exchange.spec.ts index b934f3f8..c3aefdeb 100644 --- a/packages/protocol/tests/exchange.spec.ts +++ b/packages/protocol/tests/exchange.spec.ts @@ -19,7 +19,7 @@ describe('Exchange', () => { rfq = Rfq.create({ metadata: { from : aliceDid.uri, - to : pfiDid.uri, + to : pfiDid.uri }, data: await DevTools.createRfqData() }) @@ -29,7 +29,7 @@ describe('Exchange', () => { metadata: { from : aliceDid.uri, to : pfiDid.uri, - exchangeId : rfq.metadata.exchangeId, + exchangeId : rfq.metadata.exchangeId }, data: { reason: 'I dont like u anymore' @@ -51,7 +51,7 @@ describe('Exchange', () => { metadata: { from : pfiDid.uri, to : aliceDid.uri, - exchangeId : rfq.metadata.exchangeId, + exchangeId : rfq.metadata.exchangeId }, data: { reason: 'I dont like u anymore' @@ -72,7 +72,7 @@ describe('Exchange', () => { metadata: { from : pfiDid.uri, to : aliceDid.uri, - exchangeId : rfq.metadata.exchangeId, + exchangeId : rfq.metadata.exchangeId }, data: { orderStatus: 'Done' @@ -137,7 +137,7 @@ describe('Exchange', () => { metadata: { from : aliceDid.uri, to : pfiDid.uri, - exchangeId : rfq.metadata.exchangeId, + exchangeId : rfq.metadata.exchangeId }, data: { reason: 'I dont like u anymore' @@ -280,6 +280,57 @@ describe('Exchange', () => { } } }) + + it('cannot add a message if the protocol versions of the new message and the exchange mismatch', async () => { + const exchange = new Exchange() + exchange.addNextMessage(rfq) + + + let quote = Quote.create({ + metadata: { + from : pfiDid.uri, + to : aliceDid.uri, + exchangeId : rfq.metadata.exchangeId, + protocol : '2.0' + }, + data: DevTools.createQuoteData() + }) + await quote.sign(pfiDid) + + try { + exchange.addNextMessage(quote) + expect.fail() + } catch (e) { + expect(e.message).to.contain('does not have matching protocol version') + expect(e.message).to.contain(rfq.metadata.protocol) + expect(e.message).to.contain('1.0') + } + }) + + it('cannot add a message if the exchangeId of the new message and the exchange mismatch', async () => { + const exchange = new Exchange() + exchange.addNextMessage(rfq) + + let quote = Quote.create({ + metadata: { + from : pfiDid.uri, + to : aliceDid.uri, + exchangeId : '123', + + }, + data: DevTools.createQuoteData() + }) + await quote.sign(pfiDid) + + try { + exchange.addNextMessage(quote) + expect.fail() + } catch (e) { + expect(e.message).to.contain('does not have matching exchange id') + expect(e.message).to.contain(rfq.metadata.exchangeId) + expect(e.message).to.contain('123') + } + }) }) }) @@ -289,7 +340,7 @@ describe('Exchange', () => { const rfq = Rfq.create({ metadata: { from : aliceDid.uri, - to : pfiDid.uri, + to : pfiDid.uri }, data: await DevTools.createRfqData() }) @@ -318,7 +369,7 @@ describe('Exchange', () => { metadata: { from : pfiDid.uri, to : aliceDid.uri, - exchangeId : rfq.metadata.exchangeId, + exchangeId : rfq.metadata.exchangeId }, data: { orderStatus: 'Done' diff --git a/packages/protocol/tests/generate-test-vectors.ts b/packages/protocol/tests/generate-test-vectors.ts index a0f9e8c6..43245699 100644 --- a/packages/protocol/tests/generate-test-vectors.ts +++ b/packages/protocol/tests/generate-test-vectors.ts @@ -1,7 +1,7 @@ import { VerifiableCredential } from '@web5/credentials' import { Close, DevTools, Message, Order, OrderStatus, Quote, Resource, Rfq } from '../src/main.js' import fs from 'fs' -import { DidDht, DidJwk } from '@web5/dids' +import { DidDht } from '@web5/dids' /** * Use this util when you are modifying or adding a new test vector to `tbdex`. @@ -36,7 +36,8 @@ const generateParseQuoteVector = async () => { metadata: { exchangeId : Message.generateId('rfq'), from : pfiDid.uri, - to : aliceDid.uri + to : aliceDid.uri, + protocol : '1.0' }, data: DevTools.createQuoteData() }) @@ -51,7 +52,7 @@ const generateParseQuoteVector = async () => { } const generateParseRfqVector = async () => { - const aliceDid = await DidJwk.create() + const aliceDid = await DidDht.create() const vc = await VerifiableCredential.create({ type : 'PuupuuCredential', issuer : aliceDid.uri, @@ -64,7 +65,7 @@ const generateParseRfqVector = async () => { const vcJwt = await vc.sign({ did: aliceDid }) const rfq = Rfq.create({ - metadata : { from: aliceDid.uri, to: pfiDid.uri }, + metadata : { from: aliceDid.uri, to: pfiDid.uri, protocol: '1.0' }, data : { offeringId : Resource.generateId('offering'), payinMethod : { @@ -98,9 +99,9 @@ const generateParseRfqVector = async () => { } const generateParseOrderVector = async () => { - const aliceDid = await DidJwk.create() + const aliceDid = await DidDht.create() const order = Order.create({ - metadata: { from: aliceDid.uri, to: pfiDid.uri, exchangeId: Message.generateId('rfq'), externalId: 'ext_1234' } + metadata: { from: aliceDid.uri, to: pfiDid.uri, exchangeId: Message.generateId('rfq'), externalId: 'ext_1234', protocol: '1.0' } }) await order.sign(aliceDid) @@ -116,7 +117,7 @@ const generateParseOrderVector = async () => { const generateParseCloseVector = async () => { const pfiDid = await DidDht.create() const close = Close.create({ - metadata : { from: pfiDid.uri, to: aliceDid.uri, exchangeId: Message.generateId('rfq') }, + metadata : { from: pfiDid.uri, to: aliceDid.uri, exchangeId: Message.generateId('rfq'), protocol: '1.0' }, data : { reason: 'The reason for closing the exchange' } @@ -133,9 +134,9 @@ const generateParseCloseVector = async () => { } const generateParseOrderStatusVector = async () => { - const pfiDid = await DidJwk.create() + const pfiDid = await DidDht.create() const orderStatus = OrderStatus.create({ - metadata : { from: pfiDid.uri, to: aliceDid.uri, exchangeId: Message.generateId('rfq') }, + metadata : { from: pfiDid.uri, to: aliceDid.uri, exchangeId: Message.generateId('rfq'), protocol: '1.0' }, data : { orderStatus: 'wee' } diff --git a/packages/protocol/tests/rfq.spec.ts b/packages/protocol/tests/rfq.spec.ts index a2836791..1d22f603 100644 --- a/packages/protocol/tests/rfq.spec.ts +++ b/packages/protocol/tests/rfq.spec.ts @@ -10,6 +10,7 @@ describe('Rfq', () => { describe('create', () => { it('creates an rfq', async () => { const aliceDid = await DidJwk.create() + const message = Rfq.create({ metadata : { from: aliceDid.uri, to: 'did:ex:pfi' }, data : await DevTools.createRfqData() @@ -19,6 +20,7 @@ describe('Rfq', () => { expect(message.exchangeId).to.exist expect(message.id).to.equal(message.exchangeId) expect(message.id).to.include('rfq_') + expect(message.metadata.protocol).to.equal('1.0') }) }) @@ -208,6 +210,27 @@ describe('Rfq', () => { } }) + it('throws an error if rfq protocol doesn\'t match the provided offering\'s protocol', async () => { + const rfq = Rfq.create({ + metadata: { + from : '', + to : 'did:ex:pfi', + protocol : '2.0' + }, + data: { + ...rfqOptions.data, + offeringId: 'ABC123456', + } + }) + try { + await rfq.verifyOfferingRequirements(offering) + expect.fail() + } catch(e) { + expect(e.message).to.include('protocol version mismatch') + } + + }) + it('throws an error if offeringId doesn\'t match the provided offering\'s id', async () => { const rfq = Rfq.create({ ...rfqOptions, diff --git a/tbdex b/tbdex index 50cbc231..d7464a65 160000 --- a/tbdex +++ b/tbdex @@ -1 +1 @@ -Subproject commit 50cbc231d0144adea31ecf3fa7c14eadbd1226f2 +Subproject commit d7464a656d28c163069b4073397a80f88193068c