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

make requiredPaymentDetails optional #139

Merged
merged 7 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 26 additions & 15 deletions packages/protocol/src/message-kinds/rfq.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -146,28 +145,40 @@ 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) {
const validate = ajv.compile(paymentMethodMatch.requiredPaymentDetails)
const isValid = validate(rfqPaymentMethod.paymentDetails)
if (isValid) {
break
if (!paymentMethodMatch.requiredPaymentDetails) {
// 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'))
} 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)
}

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

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/protocol/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand Down
114 changes: 114 additions & 0 deletions packages/protocol/tests/rfq.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,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,
Expand Down Expand Up @@ -341,6 +382,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.uri },
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.uri,
to : pfi.uri,
},
data: rfqData,
})
await rfq.sign(alice)

await rfq.verifyOfferingRequirements(offering)
})
})

describe('verifyClaims', () => {
Expand Down
Loading