@mojaloop/sdk-scheme-adapter 12.3.0 → 13.0.2
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 +25 -0
- 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 +3 -3
- package/src/InboundServer/handlers.js +114 -52
- package/src/OutboundServer/api.yaml +105 -32
- package/src/OutboundServer/api_interfaces/openapi.d.ts +46 -16
- package/src/OutboundServer/api_template/components/schemas/accountsResponse.yaml +9 -0
- package/src/OutboundServer/api_template/components/schemas/partiesByIdResponse.yaml +10 -3
- package/src/OutboundServer/api_template/components/schemas/quotesPostResponse.yaml +42 -34
- package/src/OutboundServer/api_template/components/schemas/simpleTransfersPostResponse.yaml +9 -2
- 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 +1 -1
- package/src/config.js +1 -1
- 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 +261 -56
- package/src/lib/model/PartiesModel.js +15 -2
- 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/integration/lib/Outbound/parties.test.js +2 -0
- package/test/integration/lib/Outbound/quotes.test.js +2 -0
- package/test/integration/lib/Outbound/simpleTransfers.test.js +2 -0
- 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/data/commonHttpHeaders.json +1 -0
- package/test/unit/inboundApi/handlers.test.js +45 -14
- 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 +826 -157
- package/test/unit/lib/model/PartiesModel.test.js +13 -7
- package/test/unit/lib/model/QuotesModel.test.js +8 -2
- package/test/unit/lib/model/TransfersModel.test.js +8 -2
- 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,
|
|
@@ -108,9 +107,11 @@ class OutboundTransfersModel {
|
|
|
108
107
|
transitions: [
|
|
109
108
|
{ name: 'resolvePayee', from: 'start', to: 'payeeResolved' },
|
|
110
109
|
{ name: 'requestQuote', from: 'payeeResolved', to: 'quoteReceived' },
|
|
110
|
+
{ name: 'requestQuote', from: 'start', to: 'quoteReceived' },
|
|
111
111
|
{ name: 'executeTransfer', from: 'quoteReceived', to: 'succeeded' },
|
|
112
112
|
{ name: 'getTransfer', to: 'succeeded' },
|
|
113
113
|
{ name: 'error', from: '*', to: 'errored' },
|
|
114
|
+
{ name: 'abort', from: '*', to: 'aborted' },
|
|
114
115
|
],
|
|
115
116
|
methods: {
|
|
116
117
|
onTransition: this._handleTransition.bind(this),
|
|
@@ -159,6 +160,13 @@ class OutboundTransfersModel {
|
|
|
159
160
|
this.data.initiatedTimestamp = new Date().toISOString();
|
|
160
161
|
}
|
|
161
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
|
+
|
|
162
170
|
this._initStateMachine(this.data.currentState);
|
|
163
171
|
}
|
|
164
172
|
|
|
@@ -176,6 +184,9 @@ class OutboundTransfersModel {
|
|
|
176
184
|
|
|
177
185
|
case 'resolvePayee':
|
|
178
186
|
// resolve the payee
|
|
187
|
+
if (this._multiplePartiesResponse) {
|
|
188
|
+
return this._resolveBatchPayees();
|
|
189
|
+
}
|
|
179
190
|
return this._resolvePayee();
|
|
180
191
|
|
|
181
192
|
case 'requestQuote':
|
|
@@ -189,6 +200,11 @@ class OutboundTransfersModel {
|
|
|
189
200
|
// prepare a transfer and wait for fulfillment
|
|
190
201
|
return this._executeTransfer();
|
|
191
202
|
|
|
203
|
+
case 'abort':
|
|
204
|
+
this._logger.log('State machine is aborting transfer');
|
|
205
|
+
this.data.abortedReason = args[0];
|
|
206
|
+
break;
|
|
207
|
+
|
|
192
208
|
case 'error':
|
|
193
209
|
this._logger.log(`State machine is erroring with error: ${util.inspect(args)}`);
|
|
194
210
|
this.data.lastError = args[0] || new Error('unspecified error');
|
|
@@ -225,17 +241,16 @@ class OutboundTransfersModel {
|
|
|
225
241
|
}
|
|
226
242
|
this.metrics.partyLookupResponses.inc();
|
|
227
243
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if(payee.errorInformation) {
|
|
244
|
+
this.data.getPartiesResponse = JSON.parse(msg);
|
|
245
|
+
if(this.data.getPartiesResponse.body && this.data.getPartiesResponse.body.errorInformation) {
|
|
231
246
|
// this is an error response to our GET /parties request
|
|
232
|
-
const err = new BackendError(`Got an error response resolving party: ${util.inspect(
|
|
233
|
-
err.mojaloopError =
|
|
234
|
-
|
|
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;
|
|
235
249
|
// cancel the timeout handler
|
|
236
250
|
clearTimeout(timeout);
|
|
237
251
|
return reject(err);
|
|
238
252
|
}
|
|
253
|
+
let payee = this.data.getPartiesResponse.body;
|
|
239
254
|
|
|
240
255
|
if(!payee.party) {
|
|
241
256
|
// we should never get a non-error response without a party, but just in case...
|
|
@@ -310,6 +325,10 @@ class OutboundTransfersModel {
|
|
|
310
325
|
this._logger.log(`Error unsubscribing (in timeout handler) ${payeeKey} ${subId}: ${e.stack || util.inspect(e)}`);
|
|
311
326
|
});
|
|
312
327
|
|
|
328
|
+
if(latencyTimerDone) {
|
|
329
|
+
latencyTimerDone();
|
|
330
|
+
}
|
|
331
|
+
|
|
313
332
|
return reject(err);
|
|
314
333
|
}, this._requestProcessingTimeoutSeconds * 1000);
|
|
315
334
|
|
|
@@ -319,11 +338,14 @@ class OutboundTransfersModel {
|
|
|
319
338
|
latencyTimerDone = this.metrics.partyLookupLatency.startTimer();
|
|
320
339
|
const res = await this._requests.getParties(this.data.to.idType, this.data.to.idValue,
|
|
321
340
|
this.data.to.idSubValue, this.data.to.fspId);
|
|
341
|
+
|
|
342
|
+
this.data.getPartiesRequest = res.originalRequest;
|
|
343
|
+
|
|
322
344
|
this.metrics.partyLookupRequests.inc();
|
|
323
345
|
this._logger.push({ peer: res }).log('Party lookup sent to peer');
|
|
324
346
|
}
|
|
325
347
|
catch(err) {
|
|
326
|
-
// cancel the
|
|
348
|
+
// cancel the timeout and unsubscribe before rejecting the promise
|
|
327
349
|
clearTimeout(timeout);
|
|
328
350
|
|
|
329
351
|
// we dont really care if the unsubscribe fails but we should log it regardless
|
|
@@ -336,6 +358,107 @@ class OutboundTransfersModel {
|
|
|
336
358
|
});
|
|
337
359
|
}
|
|
338
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
|
+
}
|
|
339
462
|
|
|
340
463
|
/**
|
|
341
464
|
* Requests a quote
|
|
@@ -396,12 +519,13 @@ class OutboundTransfersModel {
|
|
|
396
519
|
return reject(error);
|
|
397
520
|
}
|
|
398
521
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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');
|
|
402
527
|
|
|
403
|
-
this.data.
|
|
404
|
-
this.data.quoteResponseSource = quoteResponseHeaders['fspiop-source'];
|
|
528
|
+
this.data.quoteResponseSource = this.data.quoteResponse.headers['fspiop-source'];
|
|
405
529
|
|
|
406
530
|
return resolve(quote);
|
|
407
531
|
}
|
|
@@ -419,6 +543,10 @@ class OutboundTransfersModel {
|
|
|
419
543
|
this._logger.log(`Error unsubscribing (in timeout handler) ${quoteKey} ${subId}: ${e.stack || util.inspect(e)}`);
|
|
420
544
|
});
|
|
421
545
|
|
|
546
|
+
if(latencyTimerDone) {
|
|
547
|
+
latencyTimerDone();
|
|
548
|
+
}
|
|
549
|
+
|
|
422
550
|
return reject(err);
|
|
423
551
|
}, this._requestProcessingTimeoutSeconds * 1000);
|
|
424
552
|
|
|
@@ -427,6 +555,9 @@ class OutboundTransfersModel {
|
|
|
427
555
|
try {
|
|
428
556
|
latencyTimerDone = this.metrics.quoteRequestLatency.startTimer();
|
|
429
557
|
const res = await this._requests.postQuotes(quote, this.data.to.fspId);
|
|
558
|
+
|
|
559
|
+
this.data.quoteRequest = res.originalRequest;
|
|
560
|
+
|
|
430
561
|
this.metrics.quoteRequests.inc();
|
|
431
562
|
this._logger.push({ res }).log('Quote request sent to peer');
|
|
432
563
|
}
|
|
@@ -510,13 +641,13 @@ class OutboundTransfersModel {
|
|
|
510
641
|
|
|
511
642
|
const subId = await this._cache.subscribe(transferKey, async (cn, msg, subId) => {
|
|
512
643
|
try {
|
|
513
|
-
let error;
|
|
514
|
-
let message = JSON.parse(msg);
|
|
515
|
-
|
|
516
644
|
if(latencyTimerDone) {
|
|
517
645
|
latencyTimerDone();
|
|
518
646
|
}
|
|
519
647
|
|
|
648
|
+
let error;
|
|
649
|
+
let message = JSON.parse(msg);
|
|
650
|
+
|
|
520
651
|
if (message.type === 'transferFulfil') {
|
|
521
652
|
this.metrics.transferFulfils.inc();
|
|
522
653
|
|
|
@@ -529,10 +660,10 @@ class OutboundTransfersModel {
|
|
|
529
660
|
}
|
|
530
661
|
}
|
|
531
662
|
} else if (message.type === 'transferError') {
|
|
532
|
-
error = new BackendError(`Got an error response preparing transfer: ${util.inspect(message.data, { depth: Infinity })}`, 500);
|
|
533
|
-
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;
|
|
534
665
|
} else {
|
|
535
|
-
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}.`);
|
|
536
667
|
return;
|
|
537
668
|
}
|
|
538
669
|
|
|
@@ -549,14 +680,29 @@ class OutboundTransfersModel {
|
|
|
549
680
|
}
|
|
550
681
|
|
|
551
682
|
const fulfil = message.data;
|
|
552
|
-
this._logger.push({ fulfil }).log('Transfer fulfil received');
|
|
683
|
+
this._logger.push({ fulfil: fulfil.body }).log('Transfer fulfil received');
|
|
553
684
|
this.data.fulfil = fulfil;
|
|
554
|
-
|
|
555
|
-
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)) {
|
|
556
686
|
throw new Error('Invalid fulfilment received from peer DFSP.');
|
|
557
687
|
}
|
|
558
|
-
|
|
559
|
-
|
|
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);
|
|
560
706
|
}
|
|
561
707
|
catch(err) {
|
|
562
708
|
return reject(err);
|
|
@@ -572,6 +718,10 @@ class OutboundTransfersModel {
|
|
|
572
718
|
this._logger.log(`Error unsubscribing (in timeout handler) ${transferKey} ${subId}: ${e.stack || util.inspect(e)}`);
|
|
573
719
|
});
|
|
574
720
|
|
|
721
|
+
if(latencyTimerDone) {
|
|
722
|
+
latencyTimerDone();
|
|
723
|
+
}
|
|
724
|
+
|
|
575
725
|
return reject(err);
|
|
576
726
|
}, this._requestProcessingTimeoutSeconds * 1000);
|
|
577
727
|
|
|
@@ -580,11 +730,14 @@ class OutboundTransfersModel {
|
|
|
580
730
|
try {
|
|
581
731
|
latencyTimerDone = this.metrics.transferLatency.startTimer();
|
|
582
732
|
const res = await this._requests.postTransfers(prepare, this.data.quoteResponseSource);
|
|
733
|
+
|
|
734
|
+
this.data.prepare = res.originalRequest;
|
|
735
|
+
|
|
583
736
|
this.metrics.transferPrepares.inc();
|
|
584
737
|
this._logger.push({ res }).log('Transfer prepare sent to peer');
|
|
585
738
|
}
|
|
586
739
|
catch(err) {
|
|
587
|
-
// cancel the
|
|
740
|
+
// cancel the timeout and unsubscribe before rejecting the promise
|
|
588
741
|
clearTimeout(timeout);
|
|
589
742
|
|
|
590
743
|
// we dont really care if the unsubscribe fails but we should log it regardless
|
|
@@ -610,32 +763,26 @@ class OutboundTransfersModel {
|
|
|
610
763
|
try {
|
|
611
764
|
let error;
|
|
612
765
|
let message = JSON.parse(msg);
|
|
613
|
-
|
|
614
766
|
if (message.type === 'transferError') {
|
|
615
|
-
error = new BackendError(`Got an error response retrieving transfer: ${util.inspect(message.data, { depth: Infinity })}`, 500);
|
|
616
|
-
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;
|
|
617
769
|
} else if (message.type !== 'transferFulfil') {
|
|
618
770
|
this._logger.push({ message }).log(`Ignoring cache notification for transfer ${transferKey}. Uknokwn message type ${message.type}.`);
|
|
619
771
|
return;
|
|
620
772
|
}
|
|
621
|
-
|
|
622
773
|
// cancel the timeout handler
|
|
623
774
|
clearTimeout(timeout);
|
|
624
|
-
|
|
625
775
|
// stop listening for transfer fulfil messages
|
|
626
776
|
this._cache.unsubscribe(transferKey, subId).catch(e => {
|
|
627
777
|
this._logger.log(`Error unsubscribing (in callback) ${transferKey} ${subId}: ${e.stack || util.inspect(e)}`);
|
|
628
778
|
});
|
|
629
|
-
|
|
630
779
|
if (error) {
|
|
631
780
|
return reject(error);
|
|
632
781
|
}
|
|
633
|
-
|
|
634
782
|
const fulfil = message.data;
|
|
635
|
-
this._logger.push({ fulfil }).log('Transfer fulfil received');
|
|
783
|
+
this._logger.push({ fulfil: fulfil.body }).log('Transfer fulfil received');
|
|
636
784
|
this.data.fulfil = fulfil;
|
|
637
|
-
|
|
638
|
-
return resolve(this.data);
|
|
785
|
+
return resolve(this.data.fulfil);
|
|
639
786
|
}
|
|
640
787
|
catch(err) {
|
|
641
788
|
return reject(err);
|
|
@@ -690,11 +837,11 @@ class OutboundTransfersModel {
|
|
|
690
837
|
// rather than the original request. In Forex cases we may have requested
|
|
691
838
|
// a RECEIVE amount in a currency we cannot send. FXP should always give us
|
|
692
839
|
// a quote response with transferAmount in the correct currency.
|
|
693
|
-
currency: this.data.quoteResponse.transferAmount.currency,
|
|
694
|
-
amount: this.data.quoteResponse.transferAmount.amount
|
|
840
|
+
currency: this.data.quoteResponse.body.transferAmount.currency,
|
|
841
|
+
amount: this.data.quoteResponse.body.transferAmount.amount
|
|
695
842
|
},
|
|
696
|
-
ilpPacket: this.data.quoteResponse.ilpPacket,
|
|
697
|
-
condition: this.data.quoteResponse.condition,
|
|
843
|
+
ilpPacket: this.data.quoteResponse.body.ilpPacket,
|
|
844
|
+
condition: this.data.quoteResponse.body.condition,
|
|
698
845
|
expiration: this._getExpirationTimestamp()
|
|
699
846
|
};
|
|
700
847
|
|
|
@@ -738,24 +885,28 @@ class OutboundTransfersModel {
|
|
|
738
885
|
|
|
739
886
|
switch(this.data.currentState) {
|
|
740
887
|
case 'payeeResolved':
|
|
741
|
-
resp.currentState =
|
|
888
|
+
resp.currentState = TransferStateEnum.WAITING_FOR_PARTY_ACCEPTANCE;
|
|
742
889
|
break;
|
|
743
890
|
|
|
744
891
|
case 'quoteReceived':
|
|
745
|
-
resp.currentState =
|
|
892
|
+
resp.currentState = TransferStateEnum.WAITING_FOR_QUOTE_ACCEPTANCE;
|
|
746
893
|
break;
|
|
747
894
|
|
|
748
895
|
case 'succeeded':
|
|
749
|
-
resp.currentState =
|
|
896
|
+
resp.currentState = TransferStateEnum.COMPLETED;
|
|
897
|
+
break;
|
|
898
|
+
|
|
899
|
+
case 'aborted':
|
|
900
|
+
resp.currentState = TransferStateEnum.ABORTED;
|
|
750
901
|
break;
|
|
751
902
|
|
|
752
903
|
case 'errored':
|
|
753
|
-
resp.currentState =
|
|
904
|
+
resp.currentState = TransferStateEnum.ERROR_OCCURRED;
|
|
754
905
|
break;
|
|
755
906
|
|
|
756
907
|
default:
|
|
757
908
|
this._logger.log(`Transfer model response being returned from an unexpected state: ${this.data.currentState}. Returning ERROR_OCCURRED state`);
|
|
758
|
-
resp.currentState =
|
|
909
|
+
resp.currentState = TransferStateEnum.ERROR_OCCURRED;
|
|
759
910
|
break;
|
|
760
911
|
}
|
|
761
912
|
|
|
@@ -769,7 +920,7 @@ class OutboundTransfersModel {
|
|
|
769
920
|
async _save() {
|
|
770
921
|
try {
|
|
771
922
|
this.data.currentState = this.stateMachine.state;
|
|
772
|
-
const res = await this._cache.set(`
|
|
923
|
+
const res = await this._cache.set(`transferModel_out_${this.data.transferId}`, this.data);
|
|
773
924
|
this._logger.push({ res }).log('Persisted transfer model in cache');
|
|
774
925
|
}
|
|
775
926
|
catch(err) {
|
|
@@ -786,7 +937,8 @@ class OutboundTransfersModel {
|
|
|
786
937
|
*/
|
|
787
938
|
async load(transferId) {
|
|
788
939
|
try {
|
|
789
|
-
const data = await this._cache.get(`
|
|
940
|
+
const data = await this._cache.get(`transferModel_out_${transferId}`);
|
|
941
|
+
|
|
790
942
|
if(!data) {
|
|
791
943
|
throw new Error(`No cached data found for transferId: ${transferId}`);
|
|
792
944
|
}
|
|
@@ -802,12 +954,39 @@ class OutboundTransfersModel {
|
|
|
802
954
|
|
|
803
955
|
/**
|
|
804
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
|
|
805
959
|
*/
|
|
806
|
-
async run() {
|
|
960
|
+
async run(mergeData) {
|
|
807
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
|
+
}
|
|
808
979
|
// run transitions based on incoming state
|
|
809
980
|
switch(this.data.currentState) {
|
|
810
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
|
+
|
|
811
990
|
// next transition is to resolvePayee
|
|
812
991
|
await this.stateMachine.resolvePayee();
|
|
813
992
|
this._logger.log(`Payee resolved for transfer ${this.data.transferId}`);
|
|
@@ -820,6 +999,13 @@ class OutboundTransfersModel {
|
|
|
820
999
|
break;
|
|
821
1000
|
|
|
822
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
|
+
|
|
823
1009
|
// next transition is to requestQuote
|
|
824
1010
|
await this.stateMachine.requestQuote();
|
|
825
1011
|
this._logger.log(`Quote received for transfer ${this.data.transferId}`);
|
|
@@ -832,6 +1018,13 @@ class OutboundTransfersModel {
|
|
|
832
1018
|
break;
|
|
833
1019
|
|
|
834
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
|
+
|
|
835
1028
|
// next transition is executeTransfer
|
|
836
1029
|
await this.stateMachine.executeTransfer();
|
|
837
1030
|
this._logger.log(`Transfer ${this.data.transferId} has been completed`);
|
|
@@ -853,9 +1046,21 @@ class OutboundTransfersModel {
|
|
|
853
1046
|
await this._save();
|
|
854
1047
|
this._logger.log('State machine in errored state');
|
|
855
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;
|
|
856
1061
|
}
|
|
857
1062
|
|
|
858
|
-
// now call
|
|
1063
|
+
// now call ourselves recursively to deal with the next transition
|
|
859
1064
|
this._logger.log(`Transfer model state machine transition completed in state: ${this.stateMachine.state}. Recusring to handle next transition.`);
|
|
860
1065
|
return this.run();
|
|
861
1066
|
}
|
|
@@ -58,12 +58,25 @@ function argsValidation({ type, id, subId }) {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
/**
|
|
62
|
+
* @name reformatMessage
|
|
63
|
+
* @description reformats message received from PUB/SUB channel, it is optional method, if not specified identify function is used by default
|
|
64
|
+
* @param {object} message - message received
|
|
65
|
+
* @returns {object} - reformatted message
|
|
66
|
+
*/
|
|
67
|
+
function reformatMessage(message) {
|
|
68
|
+
return {
|
|
69
|
+
party: { ...message }
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// generate model
|
|
62
74
|
const PartiesModel = Async2SyncModel.generate({
|
|
63
75
|
modelName: 'PartiesModel',
|
|
64
76
|
channelNameMethod: channelName,
|
|
65
77
|
requestActionMethod: requestAction,
|
|
66
|
-
argsValidationMethod: argsValidation
|
|
78
|
+
argsValidationMethod: argsValidation,
|
|
79
|
+
reformatMessageMethod: reformatMessage
|
|
67
80
|
});
|
|
68
81
|
|
|
69
82
|
module.exports = PartiesModel;
|
|
@@ -10,6 +10,17 @@
|
|
|
10
10
|
|
|
11
11
|
'use strict';
|
|
12
12
|
|
|
13
|
+
const TransferStateEnum = {
|
|
14
|
+
'WAITING_FOR_PARTY_ACCEPTANCE': 'WAITING_FOR_PARTY_ACCEPTANCE',
|
|
15
|
+
'QUOTE_REQUEST_RECEIVED': 'QUOTE_REQUEST_RECEIVED',
|
|
16
|
+
'WAITING_FOR_QUOTE_ACCEPTANCE': 'WAITING_FOR_QUOTE_ACCEPTANCE',
|
|
17
|
+
'PREPARE_RECEIVED': 'PREPARE_RECEIVED',
|
|
18
|
+
'ERROR_OCCURRED': 'ERROR_OCCURRED',
|
|
19
|
+
'COMPLETED': 'COMPLETED',
|
|
20
|
+
'ABORTED': 'ABORTED',
|
|
21
|
+
'RESERVED': 'RESERVED',
|
|
22
|
+
};
|
|
23
|
+
|
|
13
24
|
class BackendError extends Error {
|
|
14
25
|
constructor(msg, httpStatusCode) {
|
|
15
26
|
super(msg);
|
|
@@ -17,10 +28,23 @@ class BackendError extends Error {
|
|
|
17
28
|
}
|
|
18
29
|
|
|
19
30
|
toJSON() {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
31
|
+
const ret = {
|
|
32
|
+
httpStatusCode: this.httpStatusCode
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// copy across any other properties
|
|
36
|
+
for(let prop in this) {
|
|
37
|
+
if(this.hasOwnProperty(prop)) {
|
|
38
|
+
ret[prop] = this[prop];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return ret;
|
|
23
43
|
}
|
|
24
44
|
}
|
|
25
45
|
|
|
26
|
-
|
|
46
|
+
|
|
47
|
+
module.exports = {
|
|
48
|
+
BackendError,
|
|
49
|
+
TransferStateEnum,
|
|
50
|
+
};
|
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
**************************************************************************/
|
|
10
10
|
|
|
11
11
|
'use strict';
|
|
12
|
-
const BackendError = require('./BackendError');
|
|
12
|
+
const { BackendError, TransferStateEnum } = require('./BackendError');
|
|
13
13
|
const PersistentStateMachine = require('./PersistentStateMachine');
|
|
14
14
|
|
|
15
15
|
module.exports = {
|
|
16
16
|
BackendError,
|
|
17
|
+
TransferStateEnum,
|
|
17
18
|
PersistentStateMachine
|
|
18
19
|
};
|
|
@@ -40,6 +40,7 @@ class MockMojaloopRequests extends MojaloopRequests {
|
|
|
40
40
|
this.postBulkTransfers = MockMojaloopRequests.__postBulkTransfers;
|
|
41
41
|
this.putBulkTransfers = MockMojaloopRequests.__putBulkTransfers;
|
|
42
42
|
this.putBulkTransfersError = MockMojaloopRequests.__putBulkTransfersError;
|
|
43
|
+
this.patchTransfers = MockMojaloopRequests.__patchTransfers;
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
MockMojaloopRequests.__postParticipants = jest.fn(() => Promise.resolve());
|
|
@@ -63,7 +64,7 @@ MockMojaloopRequests.__getBulkTransfers = jest.fn(() => Promise.resolve());
|
|
|
63
64
|
MockMojaloopRequests.__postBulkTransfers = jest.fn(() => Promise.resolve());
|
|
64
65
|
MockMojaloopRequests.__putBulkTransfers = jest.fn(() => Promise.resolve());
|
|
65
66
|
MockMojaloopRequests.__putBulkTransfersError = jest.fn(() => Promise.resolve());
|
|
66
|
-
|
|
67
|
+
MockMojaloopRequests.__patchTransfers = jest.fn(() => Promise.resolve());
|
|
67
68
|
|
|
68
69
|
class MockIlp {
|
|
69
70
|
constructor(config) {
|
|
@@ -146,5 +147,5 @@ module.exports = {
|
|
|
146
147
|
},
|
|
147
148
|
Errors,
|
|
148
149
|
WSO2Auth,
|
|
149
|
-
Logger
|
|
150
|
+
Logger,
|
|
150
151
|
};
|