@mojaloop/sdk-scheme-adapter 12.2.2 → 13.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/.env.example +3 -0
  2. package/CHANGELOG.md +26 -0
  3. package/audit-resolve.json +71 -1
  4. package/docker/ml-testing-toolkit/spec_files/api_definitions/fspiop_1.1/trigger_templates/transaction_request_followup.json +2 -2
  5. package/docker/ml-testing-toolkit/spec_files/rules_callback/default.json +7 -7
  6. package/docker/ml-testing-toolkit/spec_files/rules_response/default.json +16 -16
  7. package/docker/ml-testing-toolkit/spec_files/rules_response/default_pisp_rules.json +5 -5
  8. package/docker/ml-testing-toolkit/spec_files/rules_validation/default.json +10 -10
  9. package/package.json +4 -1
  10. package/src/ControlAgent/index.js +2 -3
  11. package/src/ControlServer/index.js +2 -2
  12. package/src/InboundServer/handlers.js +114 -52
  13. package/src/InboundServer/index.js +7 -7
  14. package/src/InboundServer/middlewares.js +2 -2
  15. package/src/OutboundServer/api.yaml +54 -3
  16. package/src/OutboundServer/api_interfaces/openapi.d.ts +24 -3
  17. package/src/OutboundServer/api_template/components/schemas/accountsResponse.yaml +9 -0
  18. package/src/OutboundServer/api_template/components/schemas/transferRequest.yaml +3 -0
  19. package/src/OutboundServer/api_template/components/schemas/transferResponse.yaml +28 -2
  20. package/src/OutboundServer/api_template/components/schemas/transferStatusResponse.yaml +8 -1
  21. package/src/OutboundServer/handlers.js +4 -1
  22. package/src/OutboundServer/index.js +10 -11
  23. package/src/config.js +29 -12
  24. package/src/index.js +198 -10
  25. package/src/lib/cache.js +110 -52
  26. package/src/lib/metrics.js +148 -0
  27. package/src/lib/model/AccountsModel.js +17 -12
  28. package/src/lib/model/Async2SyncModel.js +4 -1
  29. package/src/lib/model/InboundTransfersModel.js +170 -25
  30. package/src/lib/model/OutboundBulkQuotesModel.js +4 -1
  31. package/src/lib/model/OutboundBulkTransfersModel.js +4 -1
  32. package/src/lib/model/OutboundRequestToPayModel.js +9 -7
  33. package/src/lib/model/OutboundRequestToPayTransferModel.js +6 -3
  34. package/src/lib/model/OutboundTransfersModel.js +318 -53
  35. package/src/lib/model/PartiesModel.js +1 -1
  36. package/src/lib/model/ProxyModel/index.js +4 -2
  37. package/src/lib/model/common/BackendError.js +28 -4
  38. package/src/lib/model/common/index.js +2 -1
  39. package/src/lib/validate.js +2 -2
  40. package/test/__mocks__/@mojaloop/sdk-standard-components.js +3 -2
  41. package/test/__mocks__/redis.js +4 -0
  42. package/test/config/integration.env +5 -0
  43. package/test/integration/lib/Outbound/parties.test.js +1 -1
  44. package/test/unit/ControlServer/index.js +3 -3
  45. package/test/unit/InboundServer.test.js +10 -10
  46. package/test/unit/TestServer.test.js +11 -13
  47. package/test/unit/api/accounts/data/postAccountsErrorMojaloopResponse.json +11 -3
  48. package/test/unit/api/accounts/data/postAccountsSuccessResponse.json +14 -0
  49. package/test/unit/api/accounts/data/postAccountsSuccessResponseWithError1.json +13 -0
  50. package/test/unit/api/accounts/data/postAccountsSuccessResponseWithError2.json +18 -0
  51. package/test/unit/api/accounts/utils.js +15 -1
  52. package/test/unit/api/transfers/data/getTransfersCommittedResponse.json +18 -15
  53. package/test/unit/api/transfers/data/getTransfersErrorNotFound.json +1 -0
  54. package/test/unit/api/transfers/data/postTransfersErrorMojaloopResponse.json +9 -0
  55. package/test/unit/api/transfers/data/postTransfersErrorTimeoutResponse.json +1 -0
  56. package/test/unit/api/transfers/data/postTransfersSuccessResponse.json +74 -47
  57. package/test/unit/api/transfers/utils.js +85 -4
  58. package/test/unit/api/utils.js +4 -1
  59. package/test/unit/config.test.js +2 -2
  60. package/test/unit/data/commonHttpHeaders.json +1 -0
  61. package/test/unit/data/defaultConfig.json +23 -7
  62. package/test/unit/inboundApi/handlers.test.js +45 -14
  63. package/test/unit/index.test.js +95 -4
  64. package/test/unit/lib/model/AccountsModel.test.js +9 -6
  65. package/test/unit/lib/model/InboundTransfersModel.test.js +210 -30
  66. package/test/unit/lib/model/OutboundRequestToPayModel.test.js +1 -1
  67. package/test/unit/lib/model/OutboundRequestToPayTransferModel.test.js +3 -3
  68. package/test/unit/lib/model/OutboundTransfersModel.test.js +863 -158
  69. package/test/unit/lib/model/data/defaultConfig.json +25 -10
  70. package/test/unit/lib/model/data/mockArguments.json +97 -40
  71. package/test/unit/lib/model/data/payeeParty.json +13 -11
  72. package/test/unit/lib/model/data/quoteResponse.json +36 -25
  73. package/test/unit/lib/model/data/transferFulfil.json +5 -3
  74. package/src/lib/api/index.js +0 -12
  75. package/src/lib/randomphrase/index.js +0 -21
  76. package/src/lib/randomphrase/words.json +0 -3397
@@ -15,17 +15,9 @@ const { uuid } = require('uuidv4');
15
15
  const StateMachine = require('javascript-state-machine');
16
16
  const { Ilp, MojaloopRequests } = require('@mojaloop/sdk-standard-components');
17
17
  const shared = require('./lib/shared');
18
- const { BackendError } = require('./common');
18
+ const { BackendError, TransferStateEnum } = require('./common');
19
19
  const PartiesModel = require('./PartiesModel');
20
20
 
21
- const transferStateEnum = {
22
- 'WAITING_FOR_PARTY_ACEPTANCE': 'WAITING_FOR_PARTY_ACCEPTANCE',
23
- 'WAITING_FOR_QUOTE_ACCEPTANCE': 'WAITING_FOR_QUOTE_ACCEPTANCE',
24
- 'ERROR_OCCURRED': 'ERROR_OCCURRED',
25
- 'COMPLETED': 'COMPLETED',
26
- };
27
-
28
-
29
21
  /**
30
22
  * Models the state machine and operations required for performing an outbound transfer
31
23
  */
@@ -42,6 +34,13 @@ class OutboundTransfersModel {
42
34
  this._autoAcceptParty = config.autoAcceptParty;
43
35
  this._useQuoteSourceFSPAsTransferPayeeFSP = config.useQuoteSourceFSPAsTransferPayeeFSP;
44
36
  this._checkIlp = config.checkIlp;
37
+ this._multiplePartiesResponse = config.multiplePartiesResponse;
38
+ this._multiplePartiesResponseSeconds = config.multiplePartiesResponseSeconds;
39
+ this._sendFinalNotificationIfRequested = config.sendFinalNotificationIfRequested;
40
+
41
+ if (this._autoAcceptParty && this._multiplePartiesResponse) {
42
+ throw new Error('Conflicting config options provided: autoAcceptParty and multiplePartiesResponse');
43
+ }
45
44
 
46
45
  this._requests = new MojaloopRequests({
47
46
  logger: this._logger,
@@ -51,7 +50,10 @@ class OutboundTransfersModel {
51
50
  transfersEndpoint: config.transfersEndpoint,
52
51
  transactionRequestsEndpoint: config.transactionRequestsEndpoint,
53
52
  dfspId: config.dfspId,
54
- tls: config.tls,
53
+ tls: {
54
+ enabled: config.outbound.tls.mutualTLS.enabled,
55
+ creds: config.outbound.tls.creds,
56
+ },
55
57
  jwsSign: config.jwsSign,
56
58
  jwsSignPutParties: config.jwsSignPutParties,
57
59
  jwsSigningKey: config.jwsSigningKey,
@@ -63,6 +65,36 @@ class OutboundTransfersModel {
63
65
  secret: config.ilpSecret,
64
66
  logger: this._logger,
65
67
  });
68
+
69
+ this.metrics = {
70
+ partyLookupRequests: config.metricsClient.getCounter(
71
+ 'mojaloop_connector_outbound_party_lookup_request_count',
72
+ 'Count of outbound party lookup requests sent'),
73
+ partyLookupResponses: config.metricsClient.getCounter(
74
+ 'mojaloop_connector_outbound_party_lookup_response_count',
75
+ 'Count of responses received to outbound party lookups'),
76
+ quoteRequests: config.metricsClient.getCounter(
77
+ 'mojaloop_connector_outbound_quote_request_count',
78
+ 'Count of outbound quote requests sent'),
79
+ quoteResponses: config.metricsClient.getCounter(
80
+ 'mojaloop_connector_outbound_quote_response_count',
81
+ 'Count of responses received to outbound quote requests'),
82
+ transferPrepares: config.metricsClient.getCounter(
83
+ 'mojaloop_connector_outbound_transfer_prepare_count',
84
+ 'Count of outbound transfer prepare requests sent'),
85
+ transferFulfils: config.metricsClient.getCounter(
86
+ 'mojaloop_connector_outbound_transfer_fulfil_response_count',
87
+ 'Count of responses received to outbound transfer prepares'),
88
+ partyLookupLatency: config.metricsClient.getHistogram(
89
+ 'mojaloop_connector_outbound_party_lookup_latency',
90
+ 'Time taken for a response to a party lookup request to be received'),
91
+ quoteRequestLatency: config.metricsClient.getHistogram(
92
+ 'mojaloop_connector_outbound_quote_request_latency',
93
+ 'Time taken for a response to a quote request to be received'),
94
+ transferLatency: config.metricsClient.getHistogram(
95
+ 'mojaloop_connector_outbound_transfer_latency',
96
+ 'Time taken for a response to a transfer prepare to be received')
97
+ };
66
98
  }
67
99
 
68
100
 
@@ -75,9 +107,11 @@ class OutboundTransfersModel {
75
107
  transitions: [
76
108
  { name: 'resolvePayee', from: 'start', to: 'payeeResolved' },
77
109
  { name: 'requestQuote', from: 'payeeResolved', to: 'quoteReceived' },
110
+ { name: 'requestQuote', from: 'start', to: 'quoteReceived' },
78
111
  { name: 'executeTransfer', from: 'quoteReceived', to: 'succeeded' },
79
112
  { name: 'getTransfer', to: 'succeeded' },
80
113
  { name: 'error', from: '*', to: 'errored' },
114
+ { name: 'abort', from: '*', to: 'aborted' },
81
115
  ],
82
116
  methods: {
83
117
  onTransition: this._handleTransition.bind(this),
@@ -126,6 +160,13 @@ class OutboundTransfersModel {
126
160
  this.data.initiatedTimestamp = new Date().toISOString();
127
161
  }
128
162
 
163
+ if(!this.data.hasOwnProperty('direction')) {
164
+ this.data.direction = 'OUTBOUND';
165
+ }
166
+ if(this.data.skipPartyLookup && typeof(this.data.to.fspId) === undefined) {
167
+ throw new Error('fspId of to party must be specific id when skipPartyLookup is truthy');
168
+ }
169
+
129
170
  this._initStateMachine(this.data.currentState);
130
171
  }
131
172
 
@@ -143,6 +184,9 @@ class OutboundTransfersModel {
143
184
 
144
185
  case 'resolvePayee':
145
186
  // resolve the payee
187
+ if (this._multiplePartiesResponse) {
188
+ return this._resolveBatchPayees();
189
+ }
146
190
  return this._resolvePayee();
147
191
 
148
192
  case 'requestQuote':
@@ -156,6 +200,11 @@ class OutboundTransfersModel {
156
200
  // prepare a transfer and wait for fulfillment
157
201
  return this._executeTransfer();
158
202
 
203
+ case 'abort':
204
+ this._logger.log('State machine is aborting transfer');
205
+ this.data.abortedReason = args[0];
206
+ break;
207
+
159
208
  case 'error':
160
209
  this._logger.log(`State machine is erroring with error: ${util.inspect(args)}`);
161
210
  this.data.lastError = args[0] || new Error('unspecified error');
@@ -182,20 +231,26 @@ class OutboundTransfersModel {
182
231
  subId: this.data.to.idSubValue
183
232
  });
184
233
 
234
+ let latencyTimerDone;
235
+
185
236
  // hook up a subscriber to handle response messages
186
237
  const subId = await this._cache.subscribe(payeeKey, (cn, msg, subId) => {
187
238
  try {
188
- let payee = JSON.parse(msg);
239
+ if(latencyTimerDone) {
240
+ latencyTimerDone();
241
+ }
242
+ this.metrics.partyLookupResponses.inc();
189
243
 
190
- if(payee.errorInformation) {
244
+ this.data.getPartiesResponse = JSON.parse(msg);
245
+ if(this.data.getPartiesResponse.body && this.data.getPartiesResponse.body.errorInformation) {
191
246
  // this is an error response to our GET /parties request
192
- const err = new BackendError(`Got an error response resolving party: ${util.inspect(payee, { depth: Infinity })}`, 500);
193
- err.mojaloopError = payee;
194
-
247
+ const err = new BackendError(`Got an error response resolving party: ${util.inspect(this.data.getPartiesResponse.body, { depth: Infinity })}`, 500);
248
+ err.mojaloopError = this.data.getPartiesResponse.body;
195
249
  // cancel the timeout handler
196
250
  clearTimeout(timeout);
197
251
  return reject(err);
198
252
  }
253
+ let payee = this.data.getPartiesResponse.body;
199
254
 
200
255
  if(!payee.party) {
201
256
  // we should never get a non-error response without a party, but just in case...
@@ -270,18 +325,27 @@ class OutboundTransfersModel {
270
325
  this._logger.log(`Error unsubscribing (in timeout handler) ${payeeKey} ${subId}: ${e.stack || util.inspect(e)}`);
271
326
  });
272
327
 
328
+ if(latencyTimerDone) {
329
+ latencyTimerDone();
330
+ }
331
+
273
332
  return reject(err);
274
333
  }, this._requestProcessingTimeoutSeconds * 1000);
275
334
 
276
335
  // now we have a timeout handler and a cache subscriber hooked up we can fire off
277
336
  // a GET /parties request to the switch
278
337
  try {
338
+ latencyTimerDone = this.metrics.partyLookupLatency.startTimer();
279
339
  const res = await this._requests.getParties(this.data.to.idType, this.data.to.idValue,
280
340
  this.data.to.idSubValue, this.data.to.fspId);
341
+
342
+ this.data.getPartiesRequest = res.originalRequest;
343
+
344
+ this.metrics.partyLookupRequests.inc();
281
345
  this._logger.push({ peer: res }).log('Party lookup sent to peer');
282
346
  }
283
347
  catch(err) {
284
- // cancel the timout and unsubscribe before rejecting the promise
348
+ // cancel the timeout and unsubscribe before rejecting the promise
285
349
  clearTimeout(timeout);
286
350
 
287
351
  // we dont really care if the unsubscribe fails but we should log it regardless
@@ -294,6 +358,107 @@ class OutboundTransfersModel {
294
358
  });
295
359
  }
296
360
 
361
+ /**
362
+ * Resolves multiple payees.
363
+ * Starts the payee resolution process by sending a GET /parties request to the switch;
364
+ * then waits for a specified number of seconds and resolve payees with responses from the cache.
365
+ */
366
+ _resolveBatchPayees() {
367
+ // eslint-disable-next-line no-async-promise-executor
368
+ return new Promise(async (resolve, reject) => {
369
+ let latencyTimerDone;
370
+ // hook up a timer to handle response messages
371
+ // const timer = setTimeout((cn, msg, subId) => {
372
+ const payeeResolver = (msg) => {
373
+ this.data.getPartiesResponse = JSON.parse(msg);
374
+
375
+ if(this.data.getPartiesResponse.body.errorInformation) {
376
+ // this is an error response to our GET /parties request
377
+ const err = new BackendError(`Got an error response resolving party: ${util.inspect(this.data.getPartiesResponse.body, { depth: Infinity })}`, 500);
378
+ err.mojaloopError = this.data.getPartiesResponse.body;
379
+ throw err;
380
+ }
381
+ let payee = this.data.getPartiesResponse.body;
382
+
383
+ if(!payee.party) {
384
+ // we should never get a non-error response without a party, but just in case...
385
+ // cancel the timeout handler
386
+ throw new Error(`Resolved payee has no party object: ${util.inspect(payee)}`);
387
+ }
388
+ payee = payee.party;
389
+ // check we got the right payee and info we need
390
+ if(payee.partyIdInfo.partyIdType !== this.data.to.idType) {
391
+ throw new Error(`Expecting resolved payee party IdType to be ${this.data.to.idType} but got ${payee.partyIdInfo.partyIdType}`);
392
+ }
393
+ if(payee.partyIdInfo.partyIdentifier !== this.data.to.idValue) {
394
+ throw new Error(`Expecting resolved payee party identifier to be ${this.data.to.idValue} but got ${payee.partyIdInfo.partyIdentifier}`);
395
+ }
396
+ if(payee.partyIdInfo.partySubIdOrType !== this.data.to.idSubValue) {
397
+ throw new Error(`Expecting resolved payee party subTypeId to be ${this.data.to.idSubValue} but got ${payee.partyIdInfo.partySubIdOrType}`);
398
+ }
399
+ if(!payee.partyIdInfo.fspId) {
400
+ throw new Error(`Expecting resolved payee party to have an FSPID: ${util.inspect(payee.partyIdInfo)}`);
401
+ }
402
+ // now we got the payee, add the details to our data so we can use it
403
+ // in the quote request
404
+ const to = {};
405
+ to.fspId = payee.partyIdInfo.fspId;
406
+ if(payee.partyIdInfo.extensionList) {
407
+ to.extensionList = payee.partyIdInfo.extensionList.extension;
408
+ }
409
+ if(payee.personalInfo) {
410
+ if(payee.personalInfo.complexName) {
411
+ to.firstName = payee.personalInfo.complexName.firstName || this.data.to.firstName;
412
+ to.middleName = payee.personalInfo.complexName.middleName || this.data.to.middleName;
413
+ to.lastName = payee.personalInfo.complexName.lastName || this.data.to.lastName;
414
+ }
415
+ to.dateOfBirth = payee.personalInfo.dateOfBirth;
416
+ }
417
+ return to;
418
+ };
419
+ // listen for resolution events on the payee idType and idValue
420
+ // const payeeKey = `${this.data.to.idType}_${this.data.to.idValue}`
421
+ // + (this.data.to.idSubValue ? `_${this.data.to.idSubValue}` : '');
422
+ const payeeKey = PartiesModel.channelName({
423
+ type: this.data.to.idType,
424
+ id: this.data.to.idValue,
425
+ subId: this.data.to.idSubValue
426
+ });
427
+ const timer = setTimeout(async () => {
428
+ if(latencyTimerDone) {
429
+ latencyTimerDone();
430
+ }
431
+ this.metrics.partyLookupResponses.inc();
432
+ let payeeList;
433
+ try {
434
+ payeeList = await this._cache.members(payeeKey);
435
+ } catch (e) {
436
+ return reject(e);
437
+ }
438
+ if (!payeeList.length) {
439
+ return reject(new BackendError(`Timeout resolving payees for transfer ${this.data.transferId}`, 504));
440
+ }
441
+ this._logger.push({ payeeList }).log('Payees resolved');
442
+ this.data.to = payeeList.map(payeeResolver);
443
+ resolve();
444
+ }, this._multiplePartiesResponseSeconds * 1000);
445
+ // now we have a timeout handler we can fire off
446
+ // a GET /parties request to the switch
447
+ try {
448
+ latencyTimerDone = this.metrics.partyLookupLatency.startTimer();
449
+ const res = await this._requests.getParties(this.data.to.idType, this.data.to.idValue,
450
+ this.data.to.idSubValue);
451
+ this.data.getPartiesRequest = res.originalRequest;
452
+ this.metrics.partyLookupRequests.inc();
453
+ this._logger.push({ peer: res }).log('Party lookup sent to peer');
454
+ }
455
+ catch(err) {
456
+ // cancel the timer before rejecting the promise
457
+ clearTimeout(timer);
458
+ return reject(err);
459
+ }
460
+ });
461
+ }
297
462
 
298
463
  /**
299
464
  * Requests a quote
@@ -309,10 +474,16 @@ class OutboundTransfersModel {
309
474
 
310
475
  // listen for events on the quoteId
311
476
  const quoteKey = `qt_${quote.quoteId}`;
477
+ let latencyTimerDone;
312
478
 
313
479
  // hook up a subscriber to handle response messages
314
480
  const subId = await this._cache.subscribe(quoteKey, (cn, msg, subId) => {
315
481
  try {
482
+ if(latencyTimerDone) {
483
+ latencyTimerDone();
484
+ }
485
+ this.metrics.quoteResponses.inc();
486
+
316
487
  let error;
317
488
  let message = JSON.parse(msg);
318
489
 
@@ -348,12 +519,13 @@ class OutboundTransfersModel {
348
519
  return reject(error);
349
520
  }
350
521
 
351
- const quoteResponseBody = message.data;
352
- const quoteResponseHeaders = message.headers;
353
- this._logger.push({ quoteResponseBody }).log('Quote response received');
522
+ this.data.quoteResponse = {
523
+ headers: message.data.headers,
524
+ body: message.data.body
525
+ };
526
+ this._logger.push({ quoteResponse: this.data.quoteResponse.body }).log('Quote response received');
354
527
 
355
- this.data.quoteResponse = quoteResponseBody;
356
- this.data.quoteResponseSource = quoteResponseHeaders['fspiop-source'];
528
+ this.data.quoteResponseSource = this.data.quoteResponse.headers['fspiop-source'];
357
529
 
358
530
  return resolve(quote);
359
531
  }
@@ -371,13 +543,22 @@ class OutboundTransfersModel {
371
543
  this._logger.log(`Error unsubscribing (in timeout handler) ${quoteKey} ${subId}: ${e.stack || util.inspect(e)}`);
372
544
  });
373
545
 
546
+ if(latencyTimerDone) {
547
+ latencyTimerDone();
548
+ }
549
+
374
550
  return reject(err);
375
551
  }, this._requestProcessingTimeoutSeconds * 1000);
376
552
 
377
553
  // now we have a timeout handler and a cache subscriber hooked up we can fire off
378
554
  // a POST /quotes request to the switch
379
555
  try {
556
+ latencyTimerDone = this.metrics.quoteRequestLatency.startTimer();
380
557
  const res = await this._requests.postQuotes(quote, this.data.to.fspId);
558
+
559
+ this.data.quoteRequest = res.originalRequest;
560
+
561
+ this.metrics.quoteRequests.inc();
381
562
  this._logger.push({ res }).log('Quote request sent to peer');
382
563
  }
383
564
  catch(err) {
@@ -456,12 +637,20 @@ class OutboundTransfersModel {
456
637
  // listen for events on the transferId
457
638
  const transferKey = `tf_${this.data.transferId}`;
458
639
 
640
+ let latencyTimerDone;
641
+
459
642
  const subId = await this._cache.subscribe(transferKey, async (cn, msg, subId) => {
460
643
  try {
644
+ if(latencyTimerDone) {
645
+ latencyTimerDone();
646
+ }
647
+
461
648
  let error;
462
649
  let message = JSON.parse(msg);
463
650
 
464
651
  if (message.type === 'transferFulfil') {
652
+ this.metrics.transferFulfils.inc();
653
+
465
654
  if (this._rejectExpiredTransferFulfils) {
466
655
  const now = new Date().toISOString();
467
656
  if (now > prepare.expiration) {
@@ -471,10 +660,10 @@ class OutboundTransfersModel {
471
660
  }
472
661
  }
473
662
  } else if (message.type === 'transferError') {
474
- error = new BackendError(`Got an error response preparing transfer: ${util.inspect(message.data, { depth: Infinity })}`, 500);
475
- error.mojaloopError = message.data;
663
+ error = new BackendError(`Got an error response preparing transfer: ${util.inspect(message.data.body, { depth: Infinity })}`, 500);
664
+ error.mojaloopError = message.data.body;
476
665
  } else {
477
- this._logger.push({ message }).log(`Ignoring cache notification for transfer ${transferKey}. Uknokwn message type ${message.type}.`);
666
+ this._logger.push({ message }).log(`Ignoring cache notification for transfer ${transferKey}. Unknown message type ${message.type}.`);
478
667
  return;
479
668
  }
480
669
 
@@ -491,14 +680,29 @@ class OutboundTransfersModel {
491
680
  }
492
681
 
493
682
  const fulfil = message.data;
494
- this._logger.push({ fulfil }).log('Transfer fulfil received');
683
+ this._logger.push({ fulfil: fulfil.body }).log('Transfer fulfil received');
495
684
  this.data.fulfil = fulfil;
496
-
497
- if(this._checkIlp && !this._ilp.validateFulfil(fulfil.fulfilment, this.data.quoteResponse.condition)) {
685
+ if(this._checkIlp && !this._ilp.validateFulfil(fulfil.body.fulfilment, this.data.quoteResponse.body.condition)) {
498
686
  throw new Error('Invalid fulfilment received from peer DFSP.');
499
687
  }
500
-
501
- return resolve(fulfil);
688
+ if(this._sendFinalNotificationIfRequested && fulfil.body.transferState === 'RESERVED') {
689
+ // we need to send a PATCH notification back to say we have committed the transfer.
690
+ // Note that this is normally a switch only responsibility but the capability is
691
+ // implemented here to support testing use cases where the mojaloop-connector is
692
+ // acting in a peer-to-peer scenario and it is desirable for the other peer to
693
+ // receive this notification.
694
+ // Note that the transfer is considered committed as far as this (payer) side is concerned
695
+ // we will use the current server time as committed timestamp.
696
+ const patchNotification = {
697
+ completedTimestamp: (new Date()).toISOString(),
698
+ transferState: 'COMMITTED',
699
+ };
700
+ const res = this._requests.patchTransfers(this.data.transferId,
701
+ patchNotification, this.data.quoteResponseSource);
702
+ this.data.patch = res.originalRequest;
703
+ this._logger.log(`PATCH final notification sent to peer for transfer ${this.data.transferId}`);
704
+ }
705
+ return resolve(fulfil.body);
502
706
  }
503
707
  catch(err) {
504
708
  return reject(err);
@@ -514,17 +718,26 @@ class OutboundTransfersModel {
514
718
  this._logger.log(`Error unsubscribing (in timeout handler) ${transferKey} ${subId}: ${e.stack || util.inspect(e)}`);
515
719
  });
516
720
 
721
+ if(latencyTimerDone) {
722
+ latencyTimerDone();
723
+ }
724
+
517
725
  return reject(err);
518
726
  }, this._requestProcessingTimeoutSeconds * 1000);
519
727
 
520
728
  // now we have a timeout handler and a cache subscriber hooked up we can fire off
521
729
  // a POST /transfers request to the switch
522
730
  try {
731
+ latencyTimerDone = this.metrics.transferLatency.startTimer();
523
732
  const res = await this._requests.postTransfers(prepare, this.data.quoteResponseSource);
733
+
734
+ this.data.prepare = res.originalRequest;
735
+
736
+ this.metrics.transferPrepares.inc();
524
737
  this._logger.push({ res }).log('Transfer prepare sent to peer');
525
738
  }
526
739
  catch(err) {
527
- // cancel the timout and unsubscribe before rejecting the promise
740
+ // cancel the timeout and unsubscribe before rejecting the promise
528
741
  clearTimeout(timeout);
529
742
 
530
743
  // we dont really care if the unsubscribe fails but we should log it regardless
@@ -550,32 +763,26 @@ class OutboundTransfersModel {
550
763
  try {
551
764
  let error;
552
765
  let message = JSON.parse(msg);
553
-
554
766
  if (message.type === 'transferError') {
555
- error = new BackendError(`Got an error response retrieving transfer: ${util.inspect(message.data, { depth: Infinity })}`, 500);
556
- error.mojaloopError = message.data;
767
+ error = new BackendError(`Got an error response retrieving transfer: ${util.inspect(message.data.body, { depth: Infinity })}`, 500);
768
+ error.mojaloopError = message.data.body;
557
769
  } else if (message.type !== 'transferFulfil') {
558
770
  this._logger.push({ message }).log(`Ignoring cache notification for transfer ${transferKey}. Uknokwn message type ${message.type}.`);
559
771
  return;
560
772
  }
561
-
562
773
  // cancel the timeout handler
563
774
  clearTimeout(timeout);
564
-
565
775
  // stop listening for transfer fulfil messages
566
776
  this._cache.unsubscribe(transferKey, subId).catch(e => {
567
777
  this._logger.log(`Error unsubscribing (in callback) ${transferKey} ${subId}: ${e.stack || util.inspect(e)}`);
568
778
  });
569
-
570
779
  if (error) {
571
780
  return reject(error);
572
781
  }
573
-
574
782
  const fulfil = message.data;
575
- this._logger.push({ fulfil }).log('Transfer fulfil received');
783
+ this._logger.push({ fulfil: fulfil.body }).log('Transfer fulfil received');
576
784
  this.data.fulfil = fulfil;
577
-
578
- return resolve(this.data);
785
+ return resolve(this.data.fulfil);
579
786
  }
580
787
  catch(err) {
581
788
  return reject(err);
@@ -630,11 +837,11 @@ class OutboundTransfersModel {
630
837
  // rather than the original request. In Forex cases we may have requested
631
838
  // a RECEIVE amount in a currency we cannot send. FXP should always give us
632
839
  // a quote response with transferAmount in the correct currency.
633
- currency: this.data.quoteResponse.transferAmount.currency,
634
- amount: this.data.quoteResponse.transferAmount.amount
840
+ currency: this.data.quoteResponse.body.transferAmount.currency,
841
+ amount: this.data.quoteResponse.body.transferAmount.amount
635
842
  },
636
- ilpPacket: this.data.quoteResponse.ilpPacket,
637
- condition: this.data.quoteResponse.condition,
843
+ ilpPacket: this.data.quoteResponse.body.ilpPacket,
844
+ condition: this.data.quoteResponse.body.condition,
638
845
  expiration: this._getExpirationTimestamp()
639
846
  };
640
847
 
@@ -678,24 +885,28 @@ class OutboundTransfersModel {
678
885
 
679
886
  switch(this.data.currentState) {
680
887
  case 'payeeResolved':
681
- resp.currentState = transferStateEnum.WAITING_FOR_PARTY_ACEPTANCE;
888
+ resp.currentState = TransferStateEnum.WAITING_FOR_PARTY_ACCEPTANCE;
682
889
  break;
683
890
 
684
891
  case 'quoteReceived':
685
- resp.currentState = transferStateEnum.WAITING_FOR_QUOTE_ACCEPTANCE;
892
+ resp.currentState = TransferStateEnum.WAITING_FOR_QUOTE_ACCEPTANCE;
686
893
  break;
687
894
 
688
895
  case 'succeeded':
689
- resp.currentState = transferStateEnum.COMPLETED;
896
+ resp.currentState = TransferStateEnum.COMPLETED;
897
+ break;
898
+
899
+ case 'aborted':
900
+ resp.currentState = TransferStateEnum.ABORTED;
690
901
  break;
691
902
 
692
903
  case 'errored':
693
- resp.currentState = transferStateEnum.ERROR_OCCURRED;
904
+ resp.currentState = TransferStateEnum.ERROR_OCCURRED;
694
905
  break;
695
906
 
696
907
  default:
697
908
  this._logger.log(`Transfer model response being returned from an unexpected state: ${this.data.currentState}. Returning ERROR_OCCURRED state`);
698
- resp.currentState = transferStateEnum.ERROR_OCCURRED;
909
+ resp.currentState = TransferStateEnum.ERROR_OCCURRED;
699
910
  break;
700
911
  }
701
912
 
@@ -709,7 +920,7 @@ class OutboundTransfersModel {
709
920
  async _save() {
710
921
  try {
711
922
  this.data.currentState = this.stateMachine.state;
712
- const res = await this._cache.set(`transferModel_${this.data.transferId}`, this.data);
923
+ const res = await this._cache.set(`transferModel_out_${this.data.transferId}`, this.data);
713
924
  this._logger.push({ res }).log('Persisted transfer model in cache');
714
925
  }
715
926
  catch(err) {
@@ -726,7 +937,8 @@ class OutboundTransfersModel {
726
937
  */
727
938
  async load(transferId) {
728
939
  try {
729
- const data = await this._cache.get(`transferModel_${transferId}`);
940
+ const data = await this._cache.get(`transferModel_out_${transferId}`);
941
+
730
942
  if(!data) {
731
943
  throw new Error(`No cached data found for transferId: ${transferId}`);
732
944
  }
@@ -742,12 +954,39 @@ class OutboundTransfersModel {
742
954
 
743
955
  /**
744
956
  * Returns a promise that resolves when the state machine has reached a terminal state
957
+ *
958
+ * @param mergeDate {object} - an object to merge with the model state (data) before running the state machine
745
959
  */
746
- async run() {
960
+ async run(mergeData) {
747
961
  try {
962
+ // if we were passed a mergeData object...
963
+ // merge it with our existing state, overwriting any existing matching root level keys
964
+ if(mergeData) {
965
+ // first remove any merge keys that we do not want to allow to be changed
966
+ // note that we could do this in the swagger also. this is to put a responsibility
967
+ // on this model to defend itself.
968
+ const permittedMergeKeys = ['acceptParty', 'acceptQuote', 'amount', 'to'];
969
+ Object.keys(mergeData).forEach(k => {
970
+ if(permittedMergeKeys.indexOf(k) === -1) {
971
+ delete mergeData[k];
972
+ }
973
+ });
974
+ this.data = {
975
+ ...this.data,
976
+ ...mergeData,
977
+ };
978
+ }
748
979
  // run transitions based on incoming state
749
980
  switch(this.data.currentState) {
750
981
  case 'start':
982
+ // first transition is to resolvePayee
983
+ if(typeof(this.data.to.fspId) !== 'undefined' && this.data.skipPartyLookup) {
984
+ // we already have the payee DFSP and we have bee asked to skip party resolution
985
+ this._logger.log(`Skipping payee resolution for transfer ${this.data.transferId} as to.fspId was provided and skipPartyLookup is truthy`);
986
+ this.data.currentState = 'payeeResolved';
987
+ break;
988
+ }
989
+
751
990
  // next transition is to resolvePayee
752
991
  await this.stateMachine.resolvePayee();
753
992
  this._logger.log(`Payee resolved for transfer ${this.data.transferId}`);
@@ -760,6 +999,13 @@ class OutboundTransfersModel {
760
999
  break;
761
1000
 
762
1001
  case 'payeeResolved':
1002
+ if(!this._autoAcceptParty && !this.data.acceptParty && !this.data.skipPartyLookup) {
1003
+ // resuming after a party resolution halt, backend did not accept the party.
1004
+ await this.stateMachine.abort('Payee rejected by backend');
1005
+ await this._save();
1006
+ return this.getResponse();
1007
+ }
1008
+
763
1009
  // next transition is to requestQuote
764
1010
  await this.stateMachine.requestQuote();
765
1011
  this._logger.log(`Quote received for transfer ${this.data.transferId}`);
@@ -772,6 +1018,13 @@ class OutboundTransfersModel {
772
1018
  break;
773
1019
 
774
1020
  case 'quoteReceived':
1021
+ if(!this._autoAcceptQuotes && !this.data.acceptQuote) {
1022
+ // resuming after a party resolution halt, backend did not accept the party.
1023
+ await this.stateMachine.abort('Quote rejected by backend');
1024
+ await this._save();
1025
+ return this.getResponse();
1026
+ }
1027
+
775
1028
  // next transition is executeTransfer
776
1029
  await this.stateMachine.executeTransfer();
777
1030
  this._logger.log(`Transfer ${this.data.transferId} has been completed`);
@@ -793,9 +1046,21 @@ class OutboundTransfersModel {
793
1046
  await this._save();
794
1047
  this._logger.log('State machine in errored state');
795
1048
  return;
1049
+
1050
+ case 'aborted':
1051
+ // stopped in aborted state
1052
+ await this._save();
1053
+ this._logger.log('State machine in aborted state');
1054
+ return this.getResponse();
1055
+
1056
+ default:
1057
+ // The state is not handled here, throwing an error to avoid an infinite recursion of this function
1058
+ await this._save();
1059
+ this._logger.error(`State machine in unhandled(${this.data.currentState}) state`);
1060
+ return;
796
1061
  }
797
1062
 
798
- // now call ourslves recursively to deal with the next transition
1063
+ // now call ourselves recursively to deal with the next transition
799
1064
  this._logger.log(`Transfer model state machine transition completed in state: ${this.stateMachine.state}. Recusring to handle next transition.`);
800
1065
  return this.run();
801
1066
  }
@@ -58,7 +58,7 @@ function argsValidation({ type, id, subId }) {
58
58
  }
59
59
  }
60
60
 
61
- // generate model
61
+ // generate model
62
62
  const PartiesModel = Async2SyncModel.generate({
63
63
  modelName: 'PartiesModel',
64
64
  channelNameMethod: channelName,
@@ -35,8 +35,10 @@ class ProxyModel {
35
35
  this._requests = new MojaloopRequests({
36
36
  logger: this._logger,
37
37
  peerEndpoint: config.peerEndpoint,
38
- dfspId: config.dfspId,
39
- tls: config.tls,
38
+ tls: {
39
+ enabled: config.outbound.tls.mutualTLS.enabled,
40
+ creds: config.outbound.tls.creds,
41
+ },
40
42
  jwsSign: config.jwsSign,
41
43
  jwsSigningKey: config.jwsSigningKey,
42
44
  wso2Auth: config.wso2Auth