From 3c752ec0e3f60b0b58affdaa04c29d4ab205fdca Mon Sep 17 00:00:00 2001 From: finn Date: Tue, 16 Jan 2024 15:34:58 -0800 Subject: [PATCH 1/6] make requiredPaymentDetails optional fixes #90 --- packages/protocol/src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/protocol/src/types.ts b/packages/protocol/src/types.ts index 3f11599f..73624fcd 100644 --- a/packages/protocol/src/types.ts +++ b/packages/protocol/src/types.ts @@ -95,7 +95,7 @@ export type PaymentMethod = { /** The type of payment method. e.g. BITCOIN_ADDRESS, DEBIT_CARD etc */ kind: string /** A JSON Schema containing the fields that need to be collected in order to use this payment method */ - requiredPaymentDetails: JsonSchema + requiredPaymentDetails?: JsonSchema } /** From 5e373c7a8f1b3573fc9ccbf963d7191efdf84078 Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Fri, 19 Jan 2024 16:40:01 -0800 Subject: [PATCH 2/6] Handle requiredPaymentDetails undefined for RFQ verification --- packages/protocol/src/message-kinds/rfq.ts | 13 ++-- packages/protocol/tests/rfq.spec.ts | 79 ++++++++++++++++++++++ 2 files changed, 88 insertions(+), 4 deletions(-) diff --git a/packages/protocol/src/message-kinds/rfq.ts b/packages/protocol/src/message-kinds/rfq.ts index c6b16479..86dbce22 100644 --- a/packages/protocol/src/message-kinds/rfq.ts +++ b/packages/protocol/src/message-kinds/rfq.ts @@ -146,21 +146,26 @@ export class Rfq extends Message { const paymentMethodMatches = allowedPaymentMethods.filter(paymentMethod => paymentMethod.kind === rfqPaymentMethod.kind) if (!paymentMethodMatches.length) { - const paymentMethodKinds = allowedPaymentMethods.map(paymentMethod => paymentMethod.kind).join() + const paymentMethodKinds = allowedPaymentMethods.map(paymentMethod => paymentMethod.kind).join(', ') throw new Error( - `offering does not support rfq's ${payDirection}Method kind. (rfq) ${rfqPaymentMethod.kind} was not found in: ${paymentMethodKinds} (offering)` + `offering does not support rfq's ${payDirection}Method kind. (rfq) ${rfqPaymentMethod.kind} was not found in: [${paymentMethodKinds}] (offering)` ) } const ajv = new Ajv.default() const invalidPaymentDetailsErrors = new Set() - // Only one matching paymentMethod is needed for (const paymentMethodMatch of paymentMethodMatches) { + if (!paymentMethodMatch.requiredPaymentDetails) { + // The offering does not required any payment details for this kind of payment method + return + } + const validate = ajv.compile(paymentMethodMatch.requiredPaymentDetails) const isValid = validate(rfqPaymentMethod.paymentDetails) if (isValid) { - break + // Selected payment method matches one of the offering's allowed payment methods + return } invalidPaymentDetailsErrors.add(validate.errors) } diff --git a/packages/protocol/tests/rfq.spec.ts b/packages/protocol/tests/rfq.spec.ts index 82fcaee8..e00cbec2 100644 --- a/packages/protocol/tests/rfq.spec.ts +++ b/packages/protocol/tests/rfq.spec.ts @@ -1,5 +1,11 @@ +<<<<<<< HEAD import { VerifiableCredential } from '@web5/credentials' import { CreateRfqOptions, Offering } from '../src/main.js' +||||||| parent of c6aae4c (Handle requiredPaymentDetails undefined for RFQ verification) +import type { CreateRfqOptions, Offering } from '../src/main.js' +======= +import { CreateRfqOptions, Offering } from '../src/main.js' +>>>>>>> c6aae4c (Handle requiredPaymentDetails undefined for RFQ verification) import { Rfq, DevTools } from '../src/main.js' import { Convert } from '@web5/common' @@ -341,6 +347,79 @@ describe('Rfq', () => { expect(e.message).to.include('rfq payoutMethod paymentDetails could not be validated against offering requiredPaymentDetails') } }) + + it('accepts selected payment method if it matches one but not all of the Offerings requiredPaymentDetails of matching kind', async () => { + // scenario: An offering has two payin methods with kind 'card'. One payin method requires property 'cardNumber' and 'pin' in the RFQ's selected + // payin method. The second payin method only requires 'cardNumber'. An RFQ has selected payin method with kind 'card' and only + // payment detail 'cardNumber', so it matches the Offering's second payin method but not the first. The RFQ is valid against the offering. + const offeringData = DevTools.createOfferingData() + + // Supply Offering with two payin methods of kind 'card'. + // The first requires 'cardNumber' and 'pin'. The second only requires 'cardNumber'. + offeringData.requiredClaims = undefined + offeringData.payinMethods = [ + { + kind : 'card', + requiredPaymentDetails : { + $schema : 'http://json-schema.org/draft-07/schema', + type : 'object', + properties : { + cardNumber: { + type: 'string' + }, + pin: { + type: 'string' + }, + }, + required : ['cardNumber', 'pin'], + additionalProperties : false + } + }, + { + kind : 'card', + requiredPaymentDetails : { + $schema : 'http://json-schema.org/draft-07/schema', + type : 'object', + properties : { + cardNumber: { + type: 'string' + } + }, + required : ['cardNumber'], + additionalProperties : false + } + } + ] + + const pfi = await DevTools.createDid() + + const offering = Offering.create({ + metadata : { from: pfi.did }, + data : offeringData, + }) + await offering.sign(pfi) + + // Construct RFQ with a payin method that has payin detail 'cardNumber' + const alice = await DevTools.createDid() + const rfqData = await DevTools.createRfqData() + rfqData.offeringId = offering.metadata.id + rfqData.payinMethod = { + kind : 'card', + paymentDetails : { + cardNumber: '1234' + } + } + const rfq = Rfq.create({ + metadata: { + from : alice.did, + to : pfi.did, + }, + data: rfqData, + }) + await rfq.sign(alice) + + await rfq.verifyOfferingRequirements(offering) + }) }) describe('verifyClaims', () => { From d253636340bab132b9d89df54eec9954f317de36 Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Tue, 13 Feb 2024 15:58:00 -0800 Subject: [PATCH 3/6] Require paymentDetails be omitted if requiredPaymentDetails omitted --- packages/protocol/src/message-kinds/rfq.ts | 24 ++++++----- packages/protocol/tests/rfq.spec.ts | 47 +++++++++++++++++++--- tbdex | 2 +- 3 files changed, 57 insertions(+), 16 deletions(-) diff --git a/packages/protocol/src/message-kinds/rfq.ts b/packages/protocol/src/message-kinds/rfq.ts index 86dbce22..36a9eb64 100644 --- a/packages/protocol/src/message-kinds/rfq.ts +++ b/packages/protocol/src/message-kinds/rfq.ts @@ -132,11 +132,10 @@ export class Rfq extends Message { * * @param rfqPaymentMethod - The Rfq's selected payin/payout method being validated * @param allowedPaymentMethods - The Offering's allowed payin/payout methods + * @param payDirection - Either 'payin' or 'payout', used to provide more detailed error messages. * - * @throws if payinMethod in {@link Rfq.data} property `kind` cannot be validated against the provided offering's payinMethod kinds - * @throws if payinMethod in {@link Rfq.data} property `paymentDetails` cannot be validated against the provided offering's payinMethod requiredPaymentDetails - * @throws if payoutMethod in {@link Rfq.data} property `kind` cannot be validated against the provided offering's payoutMethod kinds - * @throws if payoutMethod in {@link Rfq.data} property `paymentDetails` cannot be validated against the provided offering's payoutMethod requiredPaymentDetails + * @throws if rfqPaymentMethod property `kind` cannot be validated against the provided offering's paymentMethod's kinds + * @throws if rfqPaymentMethod property `paymentDetails` cannot be validated against the provided offering's paymentMethod's requiredPaymentDetails */ private verifyPaymentMethod( rfqPaymentMethod: SelectedPaymentMethod, @@ -157,8 +156,14 @@ export class Rfq extends Message { for (const paymentMethodMatch of paymentMethodMatches) { if (!paymentMethodMatch.requiredPaymentDetails) { - // The offering does not required any payment details for this kind of payment method - return + // If requiredPaymentDetails is omitted, and paymentDetails is also omitted, we have a match + if (!rfqPaymentMethod.paymentDetails) { + return + } + + // paymentDetails is present even though requiredPaymentDetails is omitted. This is unsatisfactory. + invalidPaymentDetailsErrors.add(new Error('paymentDetails must be omitted when requiredPaymentDetails is omitted')) + continue } const validate = ajv.compile(paymentMethodMatch.requiredPaymentDetails) @@ -170,9 +175,10 @@ export class Rfq extends Message { invalidPaymentDetailsErrors.add(validate.errors) } - if (invalidPaymentDetailsErrors.size > 0) { - throw new Error(`rfq ${payDirection}Method paymentDetails could not be validated against offering requiredPaymentDetails. Schema validation errors: ${Array.from(invalidPaymentDetailsErrors).join()}`) - } + throw new Error( + `rfq ${payDirection}Method paymentDetails could not be validated against offering requiredPaymentDetails. ` + + `Schema validation errors: ${Array.from(invalidPaymentDetailsErrors).join()}` + ) } /** diff --git a/packages/protocol/tests/rfq.spec.ts b/packages/protocol/tests/rfq.spec.ts index e00cbec2..36ae4ccc 100644 --- a/packages/protocol/tests/rfq.spec.ts +++ b/packages/protocol/tests/rfq.spec.ts @@ -1,11 +1,5 @@ -<<<<<<< HEAD import { VerifiableCredential } from '@web5/credentials' import { CreateRfqOptions, Offering } from '../src/main.js' -||||||| parent of c6aae4c (Handle requiredPaymentDetails undefined for RFQ verification) -import type { CreateRfqOptions, Offering } from '../src/main.js' -======= -import { CreateRfqOptions, Offering } from '../src/main.js' ->>>>>>> c6aae4c (Handle requiredPaymentDetails undefined for RFQ verification) import { Rfq, DevTools } from '../src/main.js' import { Convert } from '@web5/common' @@ -287,6 +281,47 @@ describe('Rfq', () => { } }) + it('throws an error if paymentDetails is present but offering\'s requiredPaymentDetails is omitted', async () => { + offering.data.payinMethods = [{ + kind: 'CASH', + // requiredPaymentDetails deliberately omitted + }] + const rfq = Rfq.create({ + ...rfqOptions, + data: { + ...rfqOptions.data, + payinMethod: { + ...rfqOptions.data.payinMethod, // paymentDetails deliberately present + kind: 'CASH' + } + } + }) + try { + await rfq.verifyOfferingRequirements(offering) + expect.fail() + } catch(e) { + expect(e.message).to.include('paymentDetails must be omitted when requiredPaymentDetails is omitted') + } + }) + + it('succeeds if paymentDetails is omitted and offering\'s requiredPaymentDetails is omitted', async () => { + offering.data.payinMethods = [{ + kind: 'CASH', + // requiredPaymentDetails deliberately omitted + }] + const rfq = Rfq.create({ + ...rfqOptions, + data: { + ...rfqOptions.data, + payinMethod: { + // paymentDetails deliberately omitted + kind: 'CASH' + } + } + }) + await rfq.verifyOfferingRequirements(offering) + }) + it('throws an error if payinMethod paymentDetails cannot be validated against the provided offering\'s payinMethod requiredPaymentDetails', async () => { const rfq = Rfq.create({ ...rfqOptions, diff --git a/tbdex b/tbdex index 7ec25f98..ce1d8341 160000 --- a/tbdex +++ b/tbdex @@ -1 +1 @@ -Subproject commit 7ec25f981b8ae44c179b3206944ac604329561c5 +Subproject commit ce1d83413a01d620988336560fd67f700e553e36 From dafc9a7505f9d6d4071416547ee951b3cf3ec331 Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Tue, 20 Feb 2024 10:05:28 -0800 Subject: [PATCH 4/6] PR comment - else not continue --- packages/protocol/src/message-kinds/rfq.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/protocol/src/message-kinds/rfq.ts b/packages/protocol/src/message-kinds/rfq.ts index 36a9eb64..773e817d 100644 --- a/packages/protocol/src/message-kinds/rfq.ts +++ b/packages/protocol/src/message-kinds/rfq.ts @@ -163,16 +163,16 @@ export class Rfq extends Message { // paymentDetails is present even though requiredPaymentDetails is omitted. This is unsatisfactory. invalidPaymentDetailsErrors.add(new Error('paymentDetails must be omitted when requiredPaymentDetails is omitted')) - continue - } - - const validate = ajv.compile(paymentMethodMatch.requiredPaymentDetails) - const isValid = validate(rfqPaymentMethod.paymentDetails) - if (isValid) { - // Selected payment method matches one of the offering's allowed payment methods - return + } else { + // requiredPaymentDetails is present, so Rfq's payment details must match + const validate = ajv.compile(paymentMethodMatch.requiredPaymentDetails) + const isValid = validate(rfqPaymentMethod.paymentDetails) + if (isValid) { + // Selected payment method matches one of the offering's allowed payment methods + return + } + invalidPaymentDetailsErrors.add(validate.errors) } - invalidPaymentDetailsErrors.add(validate.errors) } throw new Error( From 4a6dce2e57fc8e86841fd5761d3b8649d601554f Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Tue, 20 Feb 2024 10:06:18 -0800 Subject: [PATCH 5/6] Update submodule to latest main --- tbdex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tbdex b/tbdex index ce1d8341..9a9ed692 160000 --- a/tbdex +++ b/tbdex @@ -1 +1 @@ -Subproject commit ce1d83413a01d620988336560fd67f700e553e36 +Subproject commit 9a9ed69272a92119a71655876f697a992f444dcb From c974e398c6270978c7479695c924b7c9572057ec Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Tue, 20 Feb 2024 17:03:56 -0800 Subject: [PATCH 6/6] woops --- packages/protocol/tests/rfq.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/protocol/tests/rfq.spec.ts b/packages/protocol/tests/rfq.spec.ts index 770a7bb7..63e44ed3 100644 --- a/packages/protocol/tests/rfq.spec.ts +++ b/packages/protocol/tests/rfq.spec.ts @@ -429,7 +429,7 @@ describe('Rfq', () => { const pfi = await DevTools.createDid() const offering = Offering.create({ - metadata : { from: pfi.did }, + metadata : { from: pfi.uri }, data : offeringData, }) await offering.sign(pfi) @@ -446,8 +446,8 @@ describe('Rfq', () => { } const rfq = Rfq.create({ metadata: { - from : alice.did, - to : pfi.did, + from : alice.uri, + to : pfi.uri, }, data: rfqData, })