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

Commit

Permalink
Add exchange state machine (#168)
Browse files Browse the repository at this point in the history
* Add exchange state machine

* Fix tsdoc warnings

* lint

* Changeset

* Additional tests

* Fix
  • Loading branch information
Diane Huxley authored Feb 13, 2024
1 parent 629f0c7 commit 589edc3
Show file tree
Hide file tree
Showing 5 changed files with 471 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/green-cheetahs-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tbdex/protocol": patch
---

Add exchange state machine
124 changes: 124 additions & 0 deletions packages/protocol/src/exchange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { Close, Order, OrderStatus, Quote, Rfq } from './message-kinds/index.js'
import { Message } from './message.js'
import { MessageKind } from './types.js'

/**
* State-machine for validating the order and metadata of Tbdex messages in an exchange.
*
* This state-machine does not validate the {@link Message.signature} or {@link Message.data}
* of messages in the exchange.
*
* Either add messages in order one at a time using {@link Exchange.addNextMessage},
* or add a list of unsorted messages in an exchange using {@link Exchange.addMessages}
*
* @beta
*/
export class Exchange {
/** Message sent by Alice to PFI to request a quote */
rfq: Rfq | undefined
/** Message sent by the PFI in response to an RFQ */
quote: Quote | undefined
/** Message sent by Alice to the PFI to accept a quote*/
order: Order | undefined
/** Message sent by the PFI to Alice to convet the current status of the order */
orderstatus: OrderStatus | undefined
/** Message sent by either the PFI or Alice to terminate an exchange */
close: Close | undefined

constructor() {}

/**
* Add a list of unsorted messages to an exchange.
* @param messages - An unsorted array of Tbdex messages in a given exchange
*/
addMessages(messages: Message[]): void {
// Sort with earliest dateCreated first
const sortedMessages = messages.sort((m1, m2) => {
const time1 = new Date(m1.metadata.createdAt).getTime()
const time2 = new Date(m2.metadata.createdAt).getTime()
return time1 - time2
})

for (const message of sortedMessages) {
this.addNextMessage(message)
}
}

/**
* Add the next message in the exchange
* @param message - The next allowed message in the exchange
* @throws if message is not a valid next message. See {@link Exchange.isValidNext}
*/
addNextMessage(message: Message): void {
if (!this.isValidNext(message.metadata.kind)) {
throw new Error(
`Could not add message (${message.metadata.id}) to exchange because ${message.metadata.kind} ` +
`is not a valid next message`
)
}

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}`
)
}

if (message.isRfq()) {
this.rfq = message
} else if (message.isQuote()) {
this.quote = message
} else if (message.isClose()) {
this.close = message
} else if (message.isOrder()) {
this.order = message
} else if (message.isOrderStatus()) {
this.orderstatus = message
} else {
// Unreachable
throw new Error('Unrecognized message kind')
}
}

/**
* Determines if the message kind is a valid next message in the current exchange
* @param messageKind - the kind of TBDex message
* @returns true if the next message in the exchange may have kind messageKind, false otherwise
*/
isValidNext(messageKind: MessageKind): boolean {
const validNext = this.latestMessage?.validNext ?? new Set<MessageKind>(['rfq'])
return validNext.has(messageKind)
}

/**
* Latest message in an exchange if there are any messages currently
*/
get latestMessage(): Message | undefined {
return this.close ??
this.orderstatus ??
this.order ??
this.quote ??
this.rfq
}

/**
* The exchangeId of all messages in the Exchange
*/
get exchangeId(): string | undefined {
return this.rfq?.metadata?.exchangeId
}

/**
* A sorted list of messages currently in the exchange.
*/
get messages(): Message[] {
const allPossibleMessages: (Message | undefined)[] = [
this.rfq,
this.quote,
this.order,
this.orderstatus,
this.close
]
return allPossibleMessages.filter((message): message is Message => message !== undefined)
}
}
1 change: 1 addition & 0 deletions packages/protocol/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Message } from './message.js'

export * from './resource-kinds/index.js'
export * from './message-kinds/index.js'
export * from './exchange.js'
export * from './did-resolver.js'
export * from './dev-tools.js'
export * from './crypto.js'
Expand Down
2 changes: 1 addition & 1 deletion packages/protocol/src/message-kinds/rfq.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export type CreateRfqOptions = {
}

/**
* Message sent by Alice to PFI to requesting for a quote (RFQ)
* Message sent by Alice to PFI to request a quote (RFQ)
* @beta
*/
export class Rfq extends Message {
Expand Down
Loading

0 comments on commit 589edc3

Please sign in to comment.