diff --git a/CODEOWNERS b/CODEOWNERS index 68d84961..0b74c31d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -9,7 +9,7 @@ # The format is described: https://github.blog/2017-07-06-introducing-code-owners/ # These owners will be the default owners for everything in the repo. -* @michaelneale @mistermoe @jiyoontbd @phoebe-lew +* @michaelneale @mistermoe @jiyoontbd @phoebe-lew @KendallWeihe # ----------------------------------------------- diff --git a/README.md b/README.md index 92e171e7..e7c61272 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,30 @@ -# tbDEX Protocol - -## Introduction +# tbDEX Protocol + +- [Introduction](#introduction) +- [tbDEX Types](#tbdex-types) + - [Resources](#resources) + - [Messages](#messages) +- [Resource Types](#resource-types) + - [`Offering`](#offering) + - [Note on `baseCurrency` and `quoteCurrency`](#note-on-basecurrency-and-quotecurrency) + - [`Offering.PaymentMethod`](#offeringpaymentmethod) +- [Message Structure](#message-structure) +- [ID for each message types](#id-for-each-message-types) +- [Message Types](#message-types) + - [`RFQ (Request For Quote)`](#rfq-request-for-quote) + - [`RFQ.PaymentMethodResponse`](#rfqpaymentmethodresponse) + - [`Close`](#close) + - [`Quote`](#quote) + - [`Quote.PaymentInstructions`](#quotepaymentinstructions) + - [`Quote.PaymentInstructions.PaymentInstruction`](#quotepaymentinstructionspaymentinstruction) + - [`QuoteResponse.QuoteError`](#quoteresponsequoteerror) + - [`OrderStatus`](#orderstatus) + - [Fields that may change in future versions of the schema](#fields-that-may-change-in-future-versions-of-the-schema) +- [tbDEX conversation sequence](#tbdex-conversation-sequence) +- [Resources](#resources-1) + + +# Introduction The central aim of this repo is to act as a playground that is set up for us to easily test the ideas we come up with as we iterate our way to what we hope is a robust protocol. Specifically, this repo is focused on fleshing out: - tbDEX message schemas @@ -106,13 +130,13 @@ The `body` of each message can be any of the following message types. ## `RFQ (Request For Quote)` > Alice -> PFI: "OK, that offering looks good. Give me a Quote against that Offering, and here is how much USD I want to trade for BTC. Here is my proof of KYC, the payment method I intend to pay you USD with, and the payment method I expect you to pay me BTC in." -| field | data type | required | description | -| --------------- | --------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -| `offeringId` | string | Y | Offering which Alice would like to get a quote for | -| `amount` | string | Y | Amount of quote currency you want to spend in order to receive base currency | -| `kycProof` | string | Y | VerifiablePresentation in JWT string format that meets the specification per PresentationDefinition in the Offering | -| `payinMethod` | PaymentMethodResponse | Y | Specify which payment method to send quote currency. | -| `payoutMethod` | PaymentMethodResponse | Y | Specify which payment method to receive base currency. | +| field | data type | required | description | +| -------------- | --------------------- | -------- | ------------------------------------------------------------------------------------------------------------------- | +| `offeringId` | string | Y | Offering which Alice would like to get a quote for | +| `amount` | string | Y | Amount of quote currency you want to spend in order to receive base currency | +| `kycProof` | string | Y | VerifiablePresentation in JWT string format that meets the specification per PresentationDefinition in the Offering | +| `payinMethod` | PaymentMethodResponse | Y | Specify which payment method to send quote currency. | +| `payoutMethod` | PaymentMethodResponse | Y | Specify which payment method to receive base currency. | ### `RFQ.PaymentMethodResponse` | field | data type | required | description | @@ -136,19 +160,20 @@ The `body` of each message can be any of the following message types. } ``` -## `QuoteResponse` -> PFI -> Alice: "OK, here's your Quote that describes how much BTC you will receive based on your RFQ. Here's the total fee in USD associated with the payment methods you selected. Here's how to pay us, and how to let us pay you, when you're ready to execute the Quote. This quote expires at X time." +## `Close` +> Alice -> PFI: "Not interested anymore." or "oops sent by accident" -_Alternatively, in the case of an error_ +> PFI -> Alice: "Can't fulfill what you sent me for whatever reason (e.g. RFQ is erroneous, don't have enough liquidity etc.)" -> PFI -> Alice: "Whoops, I wasn't able to generate a quote. Here's an error message explaining why." +a `Close` can be sent by Alice _or_ the PFI as a reply to an RFQ or a Quote -| field | data type | required | description | -| -------- | ------------------ | -------- | --------------------------------------------------------- | -| `quote` | Quote | N | Quote, if the RFQ was successful | -| `error` | QuoteError | N | Error, if the RFQ was not successful | +| Field | Data Type | Required | Description | +| -------- | --------- | -------- | ------------------------------------------------ | +| `reason` | string | N | an explanation of why the thread is being closed | + +## `Quote` +> PFI -> Alice: "OK, here's your Quote that describes how much BTC you will receive based on your RFQ. Here's the total fee in USD associated with the payment methods you selected. Here's how to pay us, and how to let us pay you, when you're ready to execute the Quote. This quote expires at X time." -### `QuoteResponse.Quote` | field | data type | required | description | | --------------------- | ------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `expiryTime` | datetime | Y | When this quote expires. Expressed as ISO8601 | @@ -156,13 +181,13 @@ _Alternatively, in the case of an error_ | `amount` | string | Y | Amount of base currency that the PFI is willing to sell in exchange for quote currency `amount` in the original RFQ | | `paymentInstructions` | PaymentInstructions | N | Object that describes how to pay the PFI, and how to get paid by the PFI, in the instances where payment is performed "out-of-band" (e.g. PFI cannot be both a merchant and a payment processor simultaneously) | -#### `QuoteResponse.Quote.PaymentInstructions` +### `Quote.PaymentInstructions` | field | data type | required | description | | -------- | ------------------ | -------- | --------------------------------------------------------- | | `payin` | PaymentInstruction | N | Link or Instruction describing how to pay the PFI. | | `payout` | PaymentInstruction | N | Link or Instruction describing how to get paid by the PFI | -#### `Quote.PaymentInstructions.PaymentInstruction` +### `Quote.PaymentInstructions.PaymentInstruction` | field | data type | required | description | | ------------- | --------- | -------- | ------------------------------------------------------------------------- | | `link` | String | N | Link to allow Alice to pay PFI, or be paid by the PFI | @@ -183,9 +208,9 @@ _Alternatively, in the case of an error_ ``` ### `QuoteResponse.QuoteError` -| field | data type | required | description | -| --------------------- | ------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `details` | string | Y | Message describing the error | +| field | data type | required | description | +| --------- | --------- | -------- | ---------------------------- | +| `details` | string | Y | Message describing the error | ```json { diff --git a/js/build/compile-validators.js b/js/build/compile-validators.js index 749d28e1..4d91a488 100644 --- a/js/build/compile-validators.js +++ b/js/build/compile-validators.js @@ -14,6 +14,7 @@ import tbdexMessage from '../../json-schemas/message.schema.json' assert { type: import offering from '../../json-schemas/offering.schema.json' assert { type: 'json' } import rfq from '../../json-schemas/rfq.schema.json' assert { type: 'json' } import quote from '../../json-schemas/quote.schema.json' assert { type: 'json' } +import close from '../../json-schemas/close.schema.json' assert { type: 'json' } import order from '../../json-schemas/order.schema.json' assert { type: 'json' } import orderStatus from '../../json-schemas/order-status.schema.json' assert { type: 'json' } @@ -30,6 +31,7 @@ const schemas = { tbdexMessage, offering, rfq, + close, quote, order, orderStatus diff --git a/js/package-lock.json b/js/package-lock.json index 5781a29f..48c40b28 100644 --- a/js/package-lock.json +++ b/js/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tbd54566975/tbdex", - "version": "0.0.9", + "version": "0.0.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tbd54566975/tbdex", - "version": "0.0.9", + "version": "0.0.10", "license": "Apache-2.0", "dependencies": { "ajv": "8.12.0", diff --git a/js/src/protocol-definitions.ts b/js/src/protocol-definitions.ts index d045f77c..4618e43e 100644 --- a/js/src/protocol-definitions.ts +++ b/js/src/protocol-definitions.ts @@ -1,3 +1,18 @@ +const CloseRules = { + $actions: [ + { + who : 'recipient', + of : 'RFQ', + can : 'write' + }, + { + who : 'author', + of : 'RFQ', + can : 'write' + }, + ], +} + export const aliceProtocolDefinition = { protocol : 'https://tbd.website/protocols/tbdex', types : { @@ -7,8 +22,14 @@ export const aliceProtocolDefinition = { 'application/json' ] }, - QuoteResponse: { - schema : 'https://tbd.website/protocols/tbdex/QuoteResponse', + Quote: { + schema : 'https://tbd.website/protocols/tbdex/Quote', + dataFormats : [ + 'application/json' + ] + }, + Close: { + schema : 'https://tbd.website/protocols/tbdex/Close', dataFormats : [ 'application/json' ] @@ -23,8 +44,8 @@ export const aliceProtocolDefinition = { structure: { // alice sends RFQs, not receives them RFQ: { - // whoever received the RFQ that Alice sent, can write back a QuoteResponse to Alice - QuoteResponse: { + // whoever received the RFQ that Alice sent, can write back a Quote to Alice + Quote: { $actions: [ { who : 'recipient', @@ -32,17 +53,21 @@ export const aliceProtocolDefinition = { can : 'write' } ], + // Alice _or_ the PFI can Close/End the thread here. + Close: CloseRules, // OrderStatus can be written to Alice's DWN by someone who wrote RFQ/QuoteResponse (i.e. PFI) OrderStatus: { $actions: [ { who : 'author', - of : 'RFQ/QuoteResponse', + of : 'RFQ/Quote', can : 'write' } ] } - } + }, + // Alice _or_ the PFI can Close/End the thread here. + Close: CloseRules } } } @@ -57,8 +82,14 @@ export const pfiProtocolDefinition = { 'application/json' ] }, - QuoteResponse: { - schema : 'https://tbd.website/protocols/tbdex/QuoteResponse', + Quote: { + schema : 'https://tbd.website/protocols/tbdex/Quote', + dataFormats : [ + 'application/json' + ] + }, + Close: { + schema : 'https://tbd.website/protocols/tbdex/Close', dataFormats : [ 'application/json' ] @@ -80,11 +111,15 @@ export const pfiProtocolDefinition = { can : 'write' } ], + // Alice _or_ the PFI can Close/End the thread here. + Close: CloseRules, // PFI is sending OUT quote responses. no one should be writing QuoteResponse to PFIs. - QuoteResponse: { + Quote: { // PFI is sending OUT OrderStatus. no one should be writing OrderStatus to PFIs. - OrderStatus: { } + OrderStatus: { }, + // Alice _or_ the PFI can Close/End the thread here. + Close: CloseRules } } } -} +} \ No newline at end of file diff --git a/js/src/types.ts b/js/src/types.ts index 998d5621..616df529 100644 --- a/js/src/types.ts +++ b/js/src/types.ts @@ -33,7 +33,8 @@ export type MessageType = MessageTypes[M] export type MessageTypes = { rfq: Rfq - quoteResponse: QuoteResponse + quote: Quote + close: Close orderStatus: OrderStatus } @@ -52,7 +53,7 @@ export type TbDEXMessage = MessageMetadata & { } export interface Rfq { - offeringId: string, + offeringId: string amount: string kycProof: string payinMethod: PaymentMethodResponse @@ -64,22 +65,15 @@ export interface PaymentMethodResponse { paymentVerifiablePresentationJwt?: string } -export type QuoteResponse = XOR - export interface Quote { - quote: { - expiryTime: string - totalFee: string - amount: string - paymentInstructions?: PaymentInstructions - } + expiryTime: string + totalFee: string + amount: string + paymentInstructions?: PaymentInstructions } -export interface QuoteError { - error: { - // add some sort of error enum too? i.e MALFORMED_RFQ, CIRCLE_ERROR, SQUARE_ERROR - details: string - } +export interface Close { + reason?: string } export interface PaymentInstructions { diff --git a/js/tests/builders.spec.ts b/js/tests/builders.spec.ts index 479dfb89..bcf97d74 100644 --- a/js/tests/builders.spec.ts +++ b/js/tests/builders.spec.ts @@ -1,4 +1,4 @@ -import type { QuoteResponse, Rfq } from '../src/types.js' +import type { Rfq } from '../src/types.js' import { expect } from 'chai' import { createMessage } from '../src/builders.js' @@ -6,24 +6,24 @@ import { createMessage } from '../src/builders.js' describe('messages builders', () => { it('can build an rfq', () => { const rfq: Rfq = { - offeringId: '123', - amount: '1000', - kycProof: 'fake-jwt', - payinMethod: { - kind: 'DEBIT_CARD', - paymentVerifiablePresentationJwt: '' + offeringId : '123', + amount : '1000', + kycProof : 'fake-jwt', + payinMethod : { + kind : 'DEBIT_CARD', + paymentVerifiablePresentationJwt : '' }, payoutMethod: { - kind: 'BITCOIN_ADDRESS', - paymentVerifiablePresentationJwt: '' + kind : 'BITCOIN_ADDRESS', + paymentVerifiablePresentationJwt : '' } } const actual = createMessage({ - to: 'alice-did', - from: 'pfi-did', - type: 'rfq', - body: rfq + to : 'alice-did', + from : 'pfi-did', + type : 'rfq', + body : rfq }) expect(actual.body).to.equal(rfq) @@ -31,43 +31,42 @@ describe('messages builders', () => { }) it('builds the expected message for an existing thread', () => { const rfq: Rfq = { - offeringId: '123', - amount: '1000', - kycProof: 'fake-jwt', - payinMethod: { - kind: 'DEBIT_CARD', - paymentVerifiablePresentationJwt: 'fake-debitcard-jwt' + offeringId : '123', + amount : '1000', + kycProof : 'fake-jwt', + payinMethod : { + kind : 'DEBIT_CARD', + paymentVerifiablePresentationJwt : 'fake-debitcard-jwt' }, payoutMethod: { - kind: 'BITCOIN_ADDRESS', - paymentVerifiablePresentationJwt: 'fake-btcaddress-jwt' + kind : 'BITCOIN_ADDRESS', + paymentVerifiablePresentationJwt : 'fake-btcaddress-jwt' } } const rfqMessage = createMessage({ - to: 'pfi-did', - from: 'alice-did', - type: 'rfq', - body: rfq + to : 'pfi-did', + from : 'alice-did', + type : 'rfq', + body : rfq }) const quote = { - expiryTime: new Date().toISOString(), - totalFee: '100', - amount: '1000', - paymentInstructions: { payin: { link: 'fake.link.com' } }, + expiryTime : new Date().toISOString(), + totalFee : '100', + amount : '1000', + paymentInstructions : { payin: { link: 'fake.link.com' } }, } - const { from, to, threadId, parentId, body } = createMessage({ - last: rfqMessage, - type: 'quoteResponse', - body: { quote } + const { from, to, threadId, parentId } = createMessage({ + last : rfqMessage, + type : 'quote', + body : quote }) expect(from).to.equal(rfqMessage.from) expect(to).to.equal(rfqMessage.to) expect(threadId).to.equal(rfqMessage.threadId) expect(parentId).to.equal(rfqMessage.id) - expect(body.error).to.be.undefined }) }) diff --git a/js/tests/validator.spec.ts b/js/tests/validator.spec.ts index 4be876f6..f807ad02 100644 --- a/js/tests/validator.spec.ts +++ b/js/tests/validator.spec.ts @@ -3,24 +3,24 @@ import { expect } from 'chai' import { SchemaValidationError, validateMessage } from '../src/validator.js' const validMessage = { - 'id': '123', - 'contextId': '123', - 'from': 'did:swanky:alice', - 'to': 'did:swanky:pfi', - 'createdTime': '2023-04-14T12:12:12Z', - 'type': 'offering', - 'body': { - 'description': 'Buy BTC with USD!', - 'pair': 'BTC_USD', - 'unitPrice': '27000.0', - 'baseFee': '1.00', - 'min': '10.00', - 'max': '1000.00', - 'presentationDefinitionJwt': 'eyJhb...MIDw', - 'payinInstruments': [ + 'id' : '123', + 'contextId' : '123', + 'from' : 'did:swanky:alice', + 'to' : 'did:swanky:pfi', + 'createdTime' : '2023-04-14T12:12:12Z', + 'type' : 'offering', + 'body' : { + 'description' : 'Buy BTC with USD!', + 'pair' : 'BTC_USD', + 'unitPrice' : '27000.0', + 'baseFee' : '1.00', + 'min' : '10.00', + 'max' : '1000.00', + 'presentationDefinitionJwt' : 'eyJhb...MIDw', + 'payinInstruments' : [ { - 'kind': 'DEBIT_CARD', - 'fee': { + 'kind' : 'DEBIT_CARD', + 'fee' : { 'flatFee': '1.0' } } @@ -34,58 +34,58 @@ const validMessage = { } const mismatchedBody = { - 'id': '123', - 'contextId': '123', - 'from': 'did:swanky:alice', - 'to': 'did:swanky:pfi', - 'createdTime': '2023-04-14T12:12:12Z', - 'type': 'rfq', - 'body': { + 'id' : '123', + 'contextId' : '123', + 'from' : 'did:swanky:alice', + 'to' : 'did:swanky:pfi', + 'createdTime' : '2023-04-14T12:12:12Z', + 'type' : 'rfq', + 'body' : { 'orderStatus': 'PENDING' } } const invalidType = { - 'id': '123', - 'contextId': '123', - 'from': 'did:swanky:alice', - 'to': 'did:swanky:pfi', - 'createdTime': 'whateva', - 'type': 'blah', - 'body': { + 'id' : '123', + 'contextId' : '123', + 'from' : 'did:swanky:alice', + 'to' : 'did:swanky:pfi', + 'createdTime' : 'whateva', + 'type' : 'blah', + 'body' : { 'orderStatus': 'PENDING' } } const missingField = { - 'id': '123', - 'contextId': '123', - 'from': 'did:swanky:alice', - 'to': 'did:swanky:pfi', - 'createdTime': '2023-04-14T12:12:12Z', - 'type': 'order', - 'body': {} + 'id' : '123', + 'contextId' : '123', + 'from' : 'did:swanky:alice', + 'to' : 'did:swanky:pfi', + 'createdTime' : '2023-04-14T12:12:12Z', + 'type' : 'order', + 'body' : {} } const numberAmounts = { - 'id': '123', - 'contextId': '123', - 'from': 'did:swanky:alice', - 'to': 'did:swanky:pfi', - 'createdTime': '2023-04-14T12:12:12Z', - 'type': 'offering', - 'body': { - 'description': 'Buy BTC with USD!', - 'pair': 'BTC_USD', - 'unitPrice': 27000.0, - 'baseFee': 1.00, - 'min': 10.00, - 'max': 1000.00, - 'presentationDefinitionJwt': 'eyJhb...MIDw', - 'payinInstruments': [ + 'id' : '123', + 'contextId' : '123', + 'from' : 'did:swanky:alice', + 'to' : 'did:swanky:pfi', + 'createdTime' : '2023-04-14T12:12:12Z', + 'type' : 'offering', + 'body' : { + 'description' : 'Buy BTC with USD!', + 'pair' : 'BTC_USD', + 'unitPrice' : 27000.0, + 'baseFee' : 1.00, + 'min' : 10.00, + 'max' : 1000.00, + 'presentationDefinitionJwt' : 'eyJhb...MIDw', + 'payinInstruments' : [ { - 'kind': 'DEBIT_CARD', - 'fee': { + 'kind' : 'DEBIT_CARD', + 'fee' : { 'flatFee': 1.0 } } diff --git a/json-schemas/close.schema.json b/json-schemas/close.schema.json new file mode 100644 index 00000000..083ce738 --- /dev/null +++ b/json-schemas/close.schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://tbdex.io/close.schema.json", + "type": "object", + "additionalProperties": false, + "properties": { + "reason": { + "type": "string" + } + } + } \ No newline at end of file