@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.
- 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 +4 -1
- package/src/ControlAgent/index.js +2 -3
- package/src/ControlServer/index.js +2 -2
- package/src/InboundServer/handlers.js +114 -52
- package/src/InboundServer/index.js +7 -7
- package/src/InboundServer/middlewares.js +2 -2
- 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 +10 -11
- package/src/config.js +29 -12
- package/src/index.js +198 -10
- package/src/lib/cache.js +110 -52
- package/src/lib/metrics.js +148 -0
- package/src/lib/model/AccountsModel.js +17 -12
- package/src/lib/model/Async2SyncModel.js +4 -1
- package/src/lib/model/InboundTransfersModel.js +170 -25
- package/src/lib/model/OutboundBulkQuotesModel.js +4 -1
- package/src/lib/model/OutboundBulkTransfersModel.js +4 -1
- package/src/lib/model/OutboundRequestToPayModel.js +9 -7
- package/src/lib/model/OutboundRequestToPayTransferModel.js +6 -3
- package/src/lib/model/OutboundTransfersModel.js +318 -53
- package/src/lib/model/PartiesModel.js +1 -1
- package/src/lib/model/ProxyModel/index.js +4 -2
- package/src/lib/model/common/BackendError.js +28 -4
- package/src/lib/model/common/index.js +2 -1
- package/src/lib/validate.js +2 -2
- package/test/__mocks__/@mojaloop/sdk-standard-components.js +3 -2
- package/test/__mocks__/redis.js +4 -0
- package/test/config/integration.env +5 -0
- package/test/integration/lib/Outbound/parties.test.js +1 -1
- package/test/unit/ControlServer/index.js +3 -3
- package/test/unit/InboundServer.test.js +10 -10
- 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/config.test.js +2 -2
- package/test/unit/data/commonHttpHeaders.json +1 -0
- package/test/unit/data/defaultConfig.json +23 -7
- package/test/unit/inboundApi/handlers.test.js +45 -14
- package/test/unit/index.test.js +95 -4
- 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 +863 -158
- package/test/unit/lib/model/data/defaultConfig.json +25 -10
- 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
- package/src/lib/api/index.js +0 -12
- package/src/lib/randomphrase/index.js +0 -21
- 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:
|
|
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
|
-
|
|
239
|
+
if(latencyTimerDone) {
|
|
240
|
+
latencyTimerDone();
|
|
241
|
+
}
|
|
242
|
+
this.metrics.partyLookupResponses.inc();
|
|
189
243
|
|
|
190
|
-
|
|
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(
|
|
193
|
-
err.mojaloopError =
|
|
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
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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.
|
|
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}.
|
|
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
|
-
|
|
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
|
|
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 =
|
|
888
|
+
resp.currentState = TransferStateEnum.WAITING_FOR_PARTY_ACCEPTANCE;
|
|
682
889
|
break;
|
|
683
890
|
|
|
684
891
|
case 'quoteReceived':
|
|
685
|
-
resp.currentState =
|
|
892
|
+
resp.currentState = TransferStateEnum.WAITING_FOR_QUOTE_ACCEPTANCE;
|
|
686
893
|
break;
|
|
687
894
|
|
|
688
895
|
case 'succeeded':
|
|
689
|
-
resp.currentState =
|
|
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 =
|
|
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 =
|
|
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(`
|
|
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(`
|
|
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
|
|
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
|
}
|
|
@@ -35,8 +35,10 @@ class ProxyModel {
|
|
|
35
35
|
this._requests = new MojaloopRequests({
|
|
36
36
|
logger: this._logger,
|
|
37
37
|
peerEndpoint: config.peerEndpoint,
|
|
38
|
-
|
|
39
|
-
|
|
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
|