@mojaloop/sdk-scheme-adapter 12.2.3 → 13.0.1
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.
- package/.env.example +3 -0
- package/CHANGELOG.md +26 -0
- package/audit-resolve.json +71 -1
- package/docker/ml-testing-toolkit/spec_files/api_definitions/fspiop_1.1/trigger_templates/transaction_request_followup.json +2 -2
- package/docker/ml-testing-toolkit/spec_files/rules_callback/default.json +7 -7
- package/docker/ml-testing-toolkit/spec_files/rules_response/default.json +16 -16
- package/docker/ml-testing-toolkit/spec_files/rules_response/default_pisp_rules.json +5 -5
- package/docker/ml-testing-toolkit/spec_files/rules_validation/default.json +10 -10
- package/package.json +5 -3
- package/src/InboundServer/handlers.js +114 -52
- package/src/OutboundServer/api.yaml +54 -3
- package/src/OutboundServer/api_interfaces/openapi.d.ts +24 -3
- package/src/OutboundServer/api_template/components/schemas/accountsResponse.yaml +9 -0
- package/src/OutboundServer/api_template/components/schemas/transferRequest.yaml +3 -0
- package/src/OutboundServer/api_template/components/schemas/transferResponse.yaml +28 -2
- package/src/OutboundServer/api_template/components/schemas/transferStatusResponse.yaml +8 -1
- package/src/OutboundServer/handlers.js +4 -1
- package/src/OutboundServer/index.js +6 -4
- package/src/config.js +1 -1
- package/src/index.js +198 -8
- package/src/lib/cache.js +110 -52
- package/src/lib/metrics.js +148 -0
- package/src/lib/model/AccountsModel.js +13 -11
- package/src/lib/model/InboundTransfersModel.js +166 -24
- package/src/lib/model/OutboundRequestToPayModel.js +5 -6
- package/src/lib/model/OutboundRequestToPayTransferModel.js +2 -2
- package/src/lib/model/OutboundTransfersModel.js +314 -52
- package/src/lib/model/PartiesModel.js +1 -1
- package/src/lib/model/common/BackendError.js +28 -4
- package/src/lib/model/common/index.js +2 -1
- package/test/__mocks__/@mojaloop/sdk-standard-components.js +3 -2
- package/test/__mocks__/redis.js +4 -0
- package/test/integration/lib/Outbound/parties.test.js +1 -1
- package/test/unit/InboundServer.test.js +9 -9
- package/test/unit/TestServer.test.js +11 -13
- package/test/unit/api/accounts/data/postAccountsErrorMojaloopResponse.json +11 -3
- package/test/unit/api/accounts/data/postAccountsSuccessResponse.json +14 -0
- package/test/unit/api/accounts/data/postAccountsSuccessResponseWithError1.json +13 -0
- package/test/unit/api/accounts/data/postAccountsSuccessResponseWithError2.json +18 -0
- package/test/unit/api/accounts/utils.js +15 -1
- package/test/unit/api/transfers/data/getTransfersCommittedResponse.json +18 -15
- package/test/unit/api/transfers/data/getTransfersErrorNotFound.json +1 -0
- package/test/unit/api/transfers/data/postTransfersErrorMojaloopResponse.json +9 -0
- package/test/unit/api/transfers/data/postTransfersErrorTimeoutResponse.json +1 -0
- package/test/unit/api/transfers/data/postTransfersSuccessResponse.json +74 -47
- package/test/unit/api/transfers/utils.js +85 -4
- package/test/unit/api/utils.js +4 -1
- package/test/unit/data/commonHttpHeaders.json +1 -0
- package/test/unit/inboundApi/handlers.test.js +45 -14
- package/test/unit/index.test.js +95 -3
- package/test/unit/lib/model/AccountsModel.test.js +9 -6
- package/test/unit/lib/model/InboundTransfersModel.test.js +210 -30
- package/test/unit/lib/model/OutboundRequestToPayModel.test.js +1 -1
- package/test/unit/lib/model/OutboundRequestToPayTransferModel.test.js +3 -3
- package/test/unit/lib/model/OutboundTransfersModel.test.js +862 -157
- package/test/unit/lib/model/data/defaultConfig.json +9 -9
- package/test/unit/lib/model/data/mockArguments.json +97 -40
- package/test/unit/lib/model/data/payeeParty.json +13 -11
- package/test/unit/lib/model/data/quoteResponse.json +36 -25
- package/test/unit/lib/model/data/transferFulfil.json +5 -3
|
@@ -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,
|
|
@@ -66,6 +65,36 @@ class OutboundTransfersModel {
|
|
|
66
65
|
secret: config.ilpSecret,
|
|
67
66
|
logger: this._logger,
|
|
68
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
|
+
};
|
|
69
98
|
}
|
|
70
99
|
|
|
71
100
|
|
|
@@ -78,9 +107,11 @@ class OutboundTransfersModel {
|
|
|
78
107
|
transitions: [
|
|
79
108
|
{ name: 'resolvePayee', from: 'start', to: 'payeeResolved' },
|
|
80
109
|
{ name: 'requestQuote', from: 'payeeResolved', to: 'quoteReceived' },
|
|
110
|
+
{ name: 'requestQuote', from: 'start', to: 'quoteReceived' },
|
|
81
111
|
{ name: 'executeTransfer', from: 'quoteReceived', to: 'succeeded' },
|
|
82
112
|
{ name: 'getTransfer', to: 'succeeded' },
|
|
83
113
|
{ name: 'error', from: '*', to: 'errored' },
|
|
114
|
+
{ name: 'abort', from: '*', to: 'aborted' },
|
|
84
115
|
],
|
|
85
116
|
methods: {
|
|
86
117
|
onTransition: this._handleTransition.bind(this),
|
|
@@ -129,6 +160,13 @@ class OutboundTransfersModel {
|
|
|
129
160
|
this.data.initiatedTimestamp = new Date().toISOString();
|
|
130
161
|
}
|
|
131
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
|
+
|
|
132
170
|
this._initStateMachine(this.data.currentState);
|
|
133
171
|
}
|
|
134
172
|
|
|
@@ -146,6 +184,9 @@ class OutboundTransfersModel {
|
|
|
146
184
|
|
|
147
185
|
case 'resolvePayee':
|
|
148
186
|
// resolve the payee
|
|
187
|
+
if (this._multiplePartiesResponse) {
|
|
188
|
+
return this._resolveBatchPayees();
|
|
189
|
+
}
|
|
149
190
|
return this._resolvePayee();
|
|
150
191
|
|
|
151
192
|
case 'requestQuote':
|
|
@@ -159,6 +200,11 @@ class OutboundTransfersModel {
|
|
|
159
200
|
// prepare a transfer and wait for fulfillment
|
|
160
201
|
return this._executeTransfer();
|
|
161
202
|
|
|
203
|
+
case 'abort':
|
|
204
|
+
this._logger.log('State machine is aborting transfer');
|
|
205
|
+
this.data.abortedReason = args[0];
|
|
206
|
+
break;
|
|
207
|
+
|
|
162
208
|
case 'error':
|
|
163
209
|
this._logger.log(`State machine is erroring with error: ${util.inspect(args)}`);
|
|
164
210
|
this.data.lastError = args[0] || new Error('unspecified error');
|
|
@@ -185,20 +231,26 @@ class OutboundTransfersModel {
|
|
|
185
231
|
subId: this.data.to.idSubValue
|
|
186
232
|
});
|
|
187
233
|
|
|
234
|
+
let latencyTimerDone;
|
|
235
|
+
|
|
188
236
|
// hook up a subscriber to handle response messages
|
|
189
237
|
const subId = await this._cache.subscribe(payeeKey, (cn, msg, subId) => {
|
|
190
238
|
try {
|
|
191
|
-
|
|
239
|
+
if(latencyTimerDone) {
|
|
240
|
+
latencyTimerDone();
|
|
241
|
+
}
|
|
242
|
+
this.metrics.partyLookupResponses.inc();
|
|
192
243
|
|
|
193
|
-
|
|
244
|
+
this.data.getPartiesResponse = JSON.parse(msg);
|
|
245
|
+
if(this.data.getPartiesResponse.body && this.data.getPartiesResponse.body.errorInformation) {
|
|
194
246
|
// this is an error response to our GET /parties request
|
|
195
|
-
const err = new BackendError(`Got an error response resolving party: ${util.inspect(
|
|
196
|
-
err.mojaloopError =
|
|
197
|
-
|
|
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;
|
|
198
249
|
// cancel the timeout handler
|
|
199
250
|
clearTimeout(timeout);
|
|
200
251
|
return reject(err);
|
|
201
252
|
}
|
|
253
|
+
let payee = this.data.getPartiesResponse.body;
|
|
202
254
|
|
|
203
255
|
if(!payee.party) {
|
|
204
256
|
// we should never get a non-error response without a party, but just in case...
|
|
@@ -273,18 +325,27 @@ class OutboundTransfersModel {
|
|
|
273
325
|
this._logger.log(`Error unsubscribing (in timeout handler) ${payeeKey} ${subId}: ${e.stack || util.inspect(e)}`);
|
|
274
326
|
});
|
|
275
327
|
|
|
328
|
+
if(latencyTimerDone) {
|
|
329
|
+
latencyTimerDone();
|
|
330
|
+
}
|
|
331
|
+
|
|
276
332
|
return reject(err);
|
|
277
333
|
}, this._requestProcessingTimeoutSeconds * 1000);
|
|
278
334
|
|
|
279
335
|
// now we have a timeout handler and a cache subscriber hooked up we can fire off
|
|
280
336
|
// a GET /parties request to the switch
|
|
281
337
|
try {
|
|
338
|
+
latencyTimerDone = this.metrics.partyLookupLatency.startTimer();
|
|
282
339
|
const res = await this._requests.getParties(this.data.to.idType, this.data.to.idValue,
|
|
283
340
|
this.data.to.idSubValue, this.data.to.fspId);
|
|
341
|
+
|
|
342
|
+
this.data.getPartiesRequest = res.originalRequest;
|
|
343
|
+
|
|
344
|
+
this.metrics.partyLookupRequests.inc();
|
|
284
345
|
this._logger.push({ peer: res }).log('Party lookup sent to peer');
|
|
285
346
|
}
|
|
286
347
|
catch(err) {
|
|
287
|
-
// cancel the
|
|
348
|
+
// cancel the timeout and unsubscribe before rejecting the promise
|
|
288
349
|
clearTimeout(timeout);
|
|
289
350
|
|
|
290
351
|
// we dont really care if the unsubscribe fails but we should log it regardless
|
|
@@ -297,6 +358,107 @@ class OutboundTransfersModel {
|
|
|
297
358
|
});
|
|
298
359
|
}
|
|
299
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
|
+
}
|
|
300
462
|
|
|
301
463
|
/**
|
|
302
464
|
* Requests a quote
|
|
@@ -312,10 +474,16 @@ class OutboundTransfersModel {
|
|
|
312
474
|
|
|
313
475
|
// listen for events on the quoteId
|
|
314
476
|
const quoteKey = `qt_${quote.quoteId}`;
|
|
477
|
+
let latencyTimerDone;
|
|
315
478
|
|
|
316
479
|
// hook up a subscriber to handle response messages
|
|
317
480
|
const subId = await this._cache.subscribe(quoteKey, (cn, msg, subId) => {
|
|
318
481
|
try {
|
|
482
|
+
if(latencyTimerDone) {
|
|
483
|
+
latencyTimerDone();
|
|
484
|
+
}
|
|
485
|
+
this.metrics.quoteResponses.inc();
|
|
486
|
+
|
|
319
487
|
let error;
|
|
320
488
|
let message = JSON.parse(msg);
|
|
321
489
|
|
|
@@ -351,12 +519,13 @@ class OutboundTransfersModel {
|
|
|
351
519
|
return reject(error);
|
|
352
520
|
}
|
|
353
521
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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');
|
|
357
527
|
|
|
358
|
-
this.data.
|
|
359
|
-
this.data.quoteResponseSource = quoteResponseHeaders['fspiop-source'];
|
|
528
|
+
this.data.quoteResponseSource = this.data.quoteResponse.headers['fspiop-source'];
|
|
360
529
|
|
|
361
530
|
return resolve(quote);
|
|
362
531
|
}
|
|
@@ -374,13 +543,22 @@ class OutboundTransfersModel {
|
|
|
374
543
|
this._logger.log(`Error unsubscribing (in timeout handler) ${quoteKey} ${subId}: ${e.stack || util.inspect(e)}`);
|
|
375
544
|
});
|
|
376
545
|
|
|
546
|
+
if(latencyTimerDone) {
|
|
547
|
+
latencyTimerDone();
|
|
548
|
+
}
|
|
549
|
+
|
|
377
550
|
return reject(err);
|
|
378
551
|
}, this._requestProcessingTimeoutSeconds * 1000);
|
|
379
552
|
|
|
380
553
|
// now we have a timeout handler and a cache subscriber hooked up we can fire off
|
|
381
554
|
// a POST /quotes request to the switch
|
|
382
555
|
try {
|
|
556
|
+
latencyTimerDone = this.metrics.quoteRequestLatency.startTimer();
|
|
383
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();
|
|
384
562
|
this._logger.push({ res }).log('Quote request sent to peer');
|
|
385
563
|
}
|
|
386
564
|
catch(err) {
|
|
@@ -459,12 +637,20 @@ class OutboundTransfersModel {
|
|
|
459
637
|
// listen for events on the transferId
|
|
460
638
|
const transferKey = `tf_${this.data.transferId}`;
|
|
461
639
|
|
|
640
|
+
let latencyTimerDone;
|
|
641
|
+
|
|
462
642
|
const subId = await this._cache.subscribe(transferKey, async (cn, msg, subId) => {
|
|
463
643
|
try {
|
|
644
|
+
if(latencyTimerDone) {
|
|
645
|
+
latencyTimerDone();
|
|
646
|
+
}
|
|
647
|
+
|
|
464
648
|
let error;
|
|
465
649
|
let message = JSON.parse(msg);
|
|
466
650
|
|
|
467
651
|
if (message.type === 'transferFulfil') {
|
|
652
|
+
this.metrics.transferFulfils.inc();
|
|
653
|
+
|
|
468
654
|
if (this._rejectExpiredTransferFulfils) {
|
|
469
655
|
const now = new Date().toISOString();
|
|
470
656
|
if (now > prepare.expiration) {
|
|
@@ -474,10 +660,10 @@ class OutboundTransfersModel {
|
|
|
474
660
|
}
|
|
475
661
|
}
|
|
476
662
|
} else if (message.type === 'transferError') {
|
|
477
|
-
error = new BackendError(`Got an error response preparing transfer: ${util.inspect(message.data, { depth: Infinity })}`, 500);
|
|
478
|
-
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;
|
|
479
665
|
} else {
|
|
480
|
-
this._logger.push({ message }).log(`Ignoring cache notification for transfer ${transferKey}.
|
|
666
|
+
this._logger.push({ message }).log(`Ignoring cache notification for transfer ${transferKey}. Unknown message type ${message.type}.`);
|
|
481
667
|
return;
|
|
482
668
|
}
|
|
483
669
|
|
|
@@ -494,14 +680,29 @@ class OutboundTransfersModel {
|
|
|
494
680
|
}
|
|
495
681
|
|
|
496
682
|
const fulfil = message.data;
|
|
497
|
-
this._logger.push({ fulfil }).log('Transfer fulfil received');
|
|
683
|
+
this._logger.push({ fulfil: fulfil.body }).log('Transfer fulfil received');
|
|
498
684
|
this.data.fulfil = fulfil;
|
|
499
|
-
|
|
500
|
-
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)) {
|
|
501
686
|
throw new Error('Invalid fulfilment received from peer DFSP.');
|
|
502
687
|
}
|
|
503
|
-
|
|
504
|
-
|
|
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);
|
|
505
706
|
}
|
|
506
707
|
catch(err) {
|
|
507
708
|
return reject(err);
|
|
@@ -517,17 +718,26 @@ class OutboundTransfersModel {
|
|
|
517
718
|
this._logger.log(`Error unsubscribing (in timeout handler) ${transferKey} ${subId}: ${e.stack || util.inspect(e)}`);
|
|
518
719
|
});
|
|
519
720
|
|
|
721
|
+
if(latencyTimerDone) {
|
|
722
|
+
latencyTimerDone();
|
|
723
|
+
}
|
|
724
|
+
|
|
520
725
|
return reject(err);
|
|
521
726
|
}, this._requestProcessingTimeoutSeconds * 1000);
|
|
522
727
|
|
|
523
728
|
// now we have a timeout handler and a cache subscriber hooked up we can fire off
|
|
524
729
|
// a POST /transfers request to the switch
|
|
525
730
|
try {
|
|
731
|
+
latencyTimerDone = this.metrics.transferLatency.startTimer();
|
|
526
732
|
const res = await this._requests.postTransfers(prepare, this.data.quoteResponseSource);
|
|
733
|
+
|
|
734
|
+
this.data.prepare = res.originalRequest;
|
|
735
|
+
|
|
736
|
+
this.metrics.transferPrepares.inc();
|
|
527
737
|
this._logger.push({ res }).log('Transfer prepare sent to peer');
|
|
528
738
|
}
|
|
529
739
|
catch(err) {
|
|
530
|
-
// cancel the
|
|
740
|
+
// cancel the timeout and unsubscribe before rejecting the promise
|
|
531
741
|
clearTimeout(timeout);
|
|
532
742
|
|
|
533
743
|
// we dont really care if the unsubscribe fails but we should log it regardless
|
|
@@ -553,32 +763,26 @@ class OutboundTransfersModel {
|
|
|
553
763
|
try {
|
|
554
764
|
let error;
|
|
555
765
|
let message = JSON.parse(msg);
|
|
556
|
-
|
|
557
766
|
if (message.type === 'transferError') {
|
|
558
|
-
error = new BackendError(`Got an error response retrieving transfer: ${util.inspect(message.data, { depth: Infinity })}`, 500);
|
|
559
|
-
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;
|
|
560
769
|
} else if (message.type !== 'transferFulfil') {
|
|
561
770
|
this._logger.push({ message }).log(`Ignoring cache notification for transfer ${transferKey}. Uknokwn message type ${message.type}.`);
|
|
562
771
|
return;
|
|
563
772
|
}
|
|
564
|
-
|
|
565
773
|
// cancel the timeout handler
|
|
566
774
|
clearTimeout(timeout);
|
|
567
|
-
|
|
568
775
|
// stop listening for transfer fulfil messages
|
|
569
776
|
this._cache.unsubscribe(transferKey, subId).catch(e => {
|
|
570
777
|
this._logger.log(`Error unsubscribing (in callback) ${transferKey} ${subId}: ${e.stack || util.inspect(e)}`);
|
|
571
778
|
});
|
|
572
|
-
|
|
573
779
|
if (error) {
|
|
574
780
|
return reject(error);
|
|
575
781
|
}
|
|
576
|
-
|
|
577
782
|
const fulfil = message.data;
|
|
578
|
-
this._logger.push({ fulfil }).log('Transfer fulfil received');
|
|
783
|
+
this._logger.push({ fulfil: fulfil.body }).log('Transfer fulfil received');
|
|
579
784
|
this.data.fulfil = fulfil;
|
|
580
|
-
|
|
581
|
-
return resolve(this.data);
|
|
785
|
+
return resolve(this.data.fulfil);
|
|
582
786
|
}
|
|
583
787
|
catch(err) {
|
|
584
788
|
return reject(err);
|
|
@@ -633,11 +837,11 @@ class OutboundTransfersModel {
|
|
|
633
837
|
// rather than the original request. In Forex cases we may have requested
|
|
634
838
|
// a RECEIVE amount in a currency we cannot send. FXP should always give us
|
|
635
839
|
// a quote response with transferAmount in the correct currency.
|
|
636
|
-
currency: this.data.quoteResponse.transferAmount.currency,
|
|
637
|
-
amount: this.data.quoteResponse.transferAmount.amount
|
|
840
|
+
currency: this.data.quoteResponse.body.transferAmount.currency,
|
|
841
|
+
amount: this.data.quoteResponse.body.transferAmount.amount
|
|
638
842
|
},
|
|
639
|
-
ilpPacket: this.data.quoteResponse.ilpPacket,
|
|
640
|
-
condition: this.data.quoteResponse.condition,
|
|
843
|
+
ilpPacket: this.data.quoteResponse.body.ilpPacket,
|
|
844
|
+
condition: this.data.quoteResponse.body.condition,
|
|
641
845
|
expiration: this._getExpirationTimestamp()
|
|
642
846
|
};
|
|
643
847
|
|
|
@@ -681,24 +885,28 @@ class OutboundTransfersModel {
|
|
|
681
885
|
|
|
682
886
|
switch(this.data.currentState) {
|
|
683
887
|
case 'payeeResolved':
|
|
684
|
-
resp.currentState =
|
|
888
|
+
resp.currentState = TransferStateEnum.WAITING_FOR_PARTY_ACCEPTANCE;
|
|
685
889
|
break;
|
|
686
890
|
|
|
687
891
|
case 'quoteReceived':
|
|
688
|
-
resp.currentState =
|
|
892
|
+
resp.currentState = TransferStateEnum.WAITING_FOR_QUOTE_ACCEPTANCE;
|
|
689
893
|
break;
|
|
690
894
|
|
|
691
895
|
case 'succeeded':
|
|
692
|
-
resp.currentState =
|
|
896
|
+
resp.currentState = TransferStateEnum.COMPLETED;
|
|
897
|
+
break;
|
|
898
|
+
|
|
899
|
+
case 'aborted':
|
|
900
|
+
resp.currentState = TransferStateEnum.ABORTED;
|
|
693
901
|
break;
|
|
694
902
|
|
|
695
903
|
case 'errored':
|
|
696
|
-
resp.currentState =
|
|
904
|
+
resp.currentState = TransferStateEnum.ERROR_OCCURRED;
|
|
697
905
|
break;
|
|
698
906
|
|
|
699
907
|
default:
|
|
700
908
|
this._logger.log(`Transfer model response being returned from an unexpected state: ${this.data.currentState}. Returning ERROR_OCCURRED state`);
|
|
701
|
-
resp.currentState =
|
|
909
|
+
resp.currentState = TransferStateEnum.ERROR_OCCURRED;
|
|
702
910
|
break;
|
|
703
911
|
}
|
|
704
912
|
|
|
@@ -712,7 +920,7 @@ class OutboundTransfersModel {
|
|
|
712
920
|
async _save() {
|
|
713
921
|
try {
|
|
714
922
|
this.data.currentState = this.stateMachine.state;
|
|
715
|
-
const res = await this._cache.set(`
|
|
923
|
+
const res = await this._cache.set(`transferModel_out_${this.data.transferId}`, this.data);
|
|
716
924
|
this._logger.push({ res }).log('Persisted transfer model in cache');
|
|
717
925
|
}
|
|
718
926
|
catch(err) {
|
|
@@ -729,7 +937,8 @@ class OutboundTransfersModel {
|
|
|
729
937
|
*/
|
|
730
938
|
async load(transferId) {
|
|
731
939
|
try {
|
|
732
|
-
const data = await this._cache.get(`
|
|
940
|
+
const data = await this._cache.get(`transferModel_out_${transferId}`);
|
|
941
|
+
|
|
733
942
|
if(!data) {
|
|
734
943
|
throw new Error(`No cached data found for transferId: ${transferId}`);
|
|
735
944
|
}
|
|
@@ -745,12 +954,39 @@ class OutboundTransfersModel {
|
|
|
745
954
|
|
|
746
955
|
/**
|
|
747
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
|
|
748
959
|
*/
|
|
749
|
-
async run() {
|
|
960
|
+
async run(mergeData) {
|
|
750
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
|
+
}
|
|
751
979
|
// run transitions based on incoming state
|
|
752
980
|
switch(this.data.currentState) {
|
|
753
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
|
+
|
|
754
990
|
// next transition is to resolvePayee
|
|
755
991
|
await this.stateMachine.resolvePayee();
|
|
756
992
|
this._logger.log(`Payee resolved for transfer ${this.data.transferId}`);
|
|
@@ -763,6 +999,13 @@ class OutboundTransfersModel {
|
|
|
763
999
|
break;
|
|
764
1000
|
|
|
765
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
|
+
|
|
766
1009
|
// next transition is to requestQuote
|
|
767
1010
|
await this.stateMachine.requestQuote();
|
|
768
1011
|
this._logger.log(`Quote received for transfer ${this.data.transferId}`);
|
|
@@ -775,6 +1018,13 @@ class OutboundTransfersModel {
|
|
|
775
1018
|
break;
|
|
776
1019
|
|
|
777
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
|
+
|
|
778
1028
|
// next transition is executeTransfer
|
|
779
1029
|
await this.stateMachine.executeTransfer();
|
|
780
1030
|
this._logger.log(`Transfer ${this.data.transferId} has been completed`);
|
|
@@ -796,9 +1046,21 @@ class OutboundTransfersModel {
|
|
|
796
1046
|
await this._save();
|
|
797
1047
|
this._logger.log('State machine in errored state');
|
|
798
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;
|
|
799
1061
|
}
|
|
800
1062
|
|
|
801
|
-
// now call
|
|
1063
|
+
// now call ourselves recursively to deal with the next transition
|
|
802
1064
|
this._logger.log(`Transfer model state machine transition completed in state: ${this.stateMachine.state}. Recusring to handle next transition.`);
|
|
803
1065
|
return this.run();
|
|
804
1066
|
}
|